Compare commits

...

51 Commits

Author SHA1 Message Date
Dennis Guse 23175902fb Release v3.7.2 2020-05-09 20:21:16 +02:00
Dennis Guse 706bd8b01f README: added link Transifex. 2020-05-09 20:21:16 +02:00
Dennis Guse dc4ac07e11 Cleanup. 2020-05-09 20:21:16 +02:00
Román 5be4129ba9 Edit marker: if user deletes photo then photo is deleted from internal storage too. Fixes #208. 2020-05-09 15:44:10 +02:00
Román f75a48ccab TrackRecordingService doesn't change the track's name when recording ends. Fixes #209. 2020-05-07 18:23:40 +02:00
Román Martínez 2815aa4b62 Merge pull request #203 from OpenTracksApp/starting_gps#200
Manually starting GPS and opening a recorded track bug fixed. Fixes #200.
2020-05-01 10:29:27 +02:00
transifex-integration[bot] 0383b0e1c7 Translate /src/main/res/values/strings.xml in nl
translation completed for the source file '/src/main/res/values/strings.xml'
on the 'nl' language.
2020-04-30 19:08:22 +02:00
Román 1601741495 Manually starting GPS and opening a recorded track bug fixed. Fixes #200. 2020-04-30 18:57:12 +02:00
john2020star be1fae956b Fix compatibility issue in styles_custom.xml (#201)
* Adding indeterminateTintMode="src_in"
* Add a new ThemeCustomNotTransparentto change the color of ProgressBar
2020-04-30 12:36:26 +02:00
Dennis Guse c7557d80bb Release v3.7.1 2020-04-28 22:59:38 +02:00
Dennis Guse ac28124664 Workaround for #200: hide start/stop GPS icon from TrackListActivity. 2020-04-28 22:38:50 +02:00
Dennis Guse 1deaf7650e Bugfix: pausing should only insert a non-null lastTrackpoint.
Found by Google Play Pre-Launch report.
2020-04-28 19:58:11 +02:00
Dennis Guse 9074fa1b6c Release v3.7.0 2020-04-28 18:36:08 +02:00
transifex-integration[bot] 5721a615a2 Translate '/src/main/res/values/strings.xml' in 'de' (#199)
* Translate /src/main/res/values/strings.xml in de

translation completed for the source file '/src/main/res/values/strings.xml'
on the 'de' language.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2020-04-28 18:34:28 +02:00
Dennis Guse fadcf1cd41 Pace/speed setting: changed label and renamed keys.
Also removed existing translations.
2020-04-28 08:23:18 +02:00
Dennis Guse a50b1ac78e Cleanup using Android Studio code analyzer. 2020-04-28 08:07:24 +02:00
Dennis Guse 9a7d4fdb36 TrackListActivity: removed start/stop GPS toast message.
Nowadays, we already have a notification in place.
2020-04-28 07:03:39 +02:00
Román Martínez 9fb51a689d Merge pull request #197 from OpenTracksApp/rgmf-issue#151
Merge issue#151.
2020-04-27 20:30:12 +02:00
Román 65bda35001 Preferred rate settings adapted to new code changes. 2020-04-27 20:25:16 +02:00
Dennis Guse 443ded86a5 Continuous integration: only test on API 29.
On Github Actions API 21 fails while it works locally.
2020-04-27 20:12:13 +02:00
Román 83b5ebb02c Added new option in Preferred rate setting: By Default.
With By Default option it will use pace or speed depending on activity type.
Fixes #151.
2020-04-27 20:02:55 +02:00
Román Martínez 240d8f6b6c Images/folders are deleted when tracks/waypoints are deleted.
This fixes #189.
2020-04-27 18:18:02 +02:00
Dennis Guse 39c5422341 Merge pull request #193 from OpenTracksApp/bluetooth
Bluetooth LE: add cycling cadence and speed sensor(s).
2020-04-27 18:14:39 +02:00
Dennis Guse 4893a2b743 Added cycling speed sensor.
Fixed speed and cadence computation.
Fixed that a sensor change in settings resulted in a disconnect for all connected sensors.
2020-04-27 07:17:46 +02:00
Dennis Guse 3408ea97f4 Refactoring: move TrackPointDataSet directly into TrackPoint. 2020-04-27 07:17:46 +02:00
Dennis Guse ddb17f61cb Bluetooth LE: implemented cycling cadence and speed sensors.
Cadence is working while speed still requires the wheel size.
2020-04-27 07:17:46 +02:00
Dennis Guse bbe182cf82 Removed handler between BluetoothConnectionManager and -RemoteSensorManager.
Toast messages should rather be sent from activities.
2020-04-27 07:17:46 +02:00
Dennis Guse b7b7bdf19b Workaround for GPS week rollover (happening every 19.7 years).
Fixes #155.

https://en.wikipedia.org/wiki/GPS_Week_Number_Rollover
2020-04-27 07:08:29 +02:00
Román Martínez 5681938a9e Merge pull request #188 from OpenTracksApp/import_export#180
Improve import/export
2020-04-23 08:21:55 +02:00
Román 74835dcef5 KMZ import/export: code improved. 2020-04-22 13:19:32 +02:00
Román ed0dc05366 KMZ exporting: marker images use waypoint id as a name. 2020-04-21 19:01:35 +02:00
Román 653eb8320a KMZ importing: look for images anywhere in KMZ file and only images referenced remain in external storage. 2020-04-21 19:01:35 +02:00
Román 54b9c4370b KML description: max/min elevation will not show if have invalid values (unknow). 2020-04-21 19:01:35 +02:00
Román fa9cb3903c KMZ exporting: checks waypoints images exists before parse kml. 2020-04-21 19:01:30 +02:00
Román 0f1d8500d1 KMZ importing: firstly copies images then parses kml (checks kml's href link to image is correct). 2020-04-21 19:00:03 +02:00
Dennis Guse 20d7fd2c03 Merge pull request #187 from OpenTracksApp/dev
Refactored TrackRecordingService.
2020-04-15 23:11:38 +02:00
Dennis Guse 1d838344dc TrackRecordingService: test inserting locations. 2020-04-15 22:37:31 +02:00
Dennis Guse fd826199e2 Tests for TrackRecordingService (pause/resume/restart). 2020-04-14 22:13:14 +02:00
Dennis Guse f485d90ef2 ContentProviderUtils.bulkInsertTrackPoint(): removed length parameter. 2020-04-14 19:42:17 +02:00
Dennis Guse ec39e67a09 Removed Waypoint default constructor used by testing.
This avoids a null check for Waypoint.getLocation().
2020-04-14 07:12:56 +02:00
Dennis Guse 1a86db0a8b Cleanup. 2020-04-13 20:23:40 +02:00
Dennis Guse 6a1fada22f TrackRecordingService: cleanup. 2020-04-13 20:23:22 +02:00
Dennis Guse 2898e06246 TrackRecordingService: move service restart handling into method. 2020-04-13 20:23:22 +02:00
Dennis Guse 0d87c65b49 TrackRecordingService: cleanup. 2020-04-13 20:23:22 +02:00
Dennis Guse 9b97cdfcd1 TrackRecordingService: cleanup.
if+return rather than if+else.
2020-04-13 20:23:22 +02:00
Dennis Guse 95feae846a Create helper functions for TrackPoints in TrackPointUtils. 2020-04-13 20:23:22 +02:00
Dennis Guse b09eeaede3 Release v3.6.2 2020-04-13 12:56:59 +02:00
Dennis Guse 35dec1634c Bugfix: charts were showing only the invalid TrackPoints.
Introduced in fbc902e830
2020-04-13 12:48:52 +02:00
Dennis Guse e49a425106 Release v3.6.1 2020-04-12 18:46:49 +02:00
Román adbad04867 URIs for waypoint's photos are correct. Fixes #183. 2020-04-12 18:27:20 +02:00
Dennis Guse 850bd2f887 Update Android gradle plugin. 2020-04-11 23:16:38 +02:00
159 changed files with 3523 additions and 1598 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
runs-on: macos-latest
strategy:
matrix:
api-level: [21, 29]
api-level: [29]
target: [default]
steps:
- name: checkout
+18 -8
View File
@@ -5,8 +5,9 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
<table>
<tr>
<th>Free</th>
<th>Paid</th>
<th>Paid (same as free)</th>
<th>Donations</th>
<th>Translations</th>
</tr>
<tr>
<td>
@@ -24,27 +25,34 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
<img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" height="45" align="middle">
</a>
</td>
<td>
<a href="https://www.transifex.com/opentracks/opentracks/">
Transifex
</a>
</td>
</tr>
</table>
## Features:
* __Tracking:__ track your sport and outdoor activities
* __Photos and Markers:__ mark interesting locations while tracking (also with photos)
* __Heart rate:__ supports Bluetooth LE sensors
* __Bluetooth LE sensors:__
* heart rate
* cycling: speed and cadence
* __Export data:__ export tracks either as KMZ (incl. photos), KML or GPX
* __No Internet access:__ Internet is not used
* __Voice announcements__
* __No advertising__
* __Privacy:__
* _does not contain any in-app analytics_
* _does not have Google Play Services_
* _does not use Google Play Services_
* _has no cloud integration_
__Only required permissions:__
* _ACCESS_FINE_LOCATION_: required to use the GPS.
_Please note:_
_Tracks_ does not contain a _map_ (as this would required Internet access).
_OpenTracks_ does not contain a _map_ (as this would required Internet access).
To show your tracks on a map, a 3rd party application (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand) or [MAPS.ME](https://play.google.com/store/apps/details?id=com.mapswithme.maps.pro)) needs to be installed.
## Custom Dashboards
@@ -53,10 +61,12 @@ The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/
## Screenshots
<div>
<img width="20%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_1.png">
<img width="20%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_2.png">
<img width="20%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_3.png">
<img width="20%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_4.png">
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_1.png">
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_2.png">
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_3.png">
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_4.png">
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_5.png">
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_6.png">
</div>
## Project history
+1 -1
View File
@@ -4,7 +4,7 @@
1. Get next _version code_ (master branch): `git rev-list HEAD --count master`
2. Decide on _version name_ (semantic versioning)
3. Manually update _version code_ and _version name_ in `AndroidManifest.xml` (used by FDroid)
3. Manually update _version code_ and _version name_ in `build.gradle`
4. Create changelog (`_version code_.txt`)
5. Create commit with all changes
6. Tag newly create commit with _version name_ (e.g., v3.2.1)
+6 -3
View File
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.android.tools.build:gradle:3.6.2'
}
}
@@ -59,11 +59,14 @@ android {
applicationName: "@string/app_name"
]
//Useful for debugging.
//versionCode getVersionCode()
versionCode 3381
//versionCode getVersionCode() //Useful for debugging.
versionName "v3.7.2"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdkVersion 21
targetSdkVersion 29
@@ -4,6 +4,7 @@ Erfasst deine Kilometer, ohne dass es jemand anderes tut.
Funktionen & Features:
* zeichnet Sport und andere Outdooraktivitäten auf
* unterstützt das Markieren interessanter Orte, inkl. Foto-Funktion
* zeichnet deine Herzfrequenz auf (unterstützt Bluetooth-LE-Sensoren)
* zeichnet deine Herzfrequenz auf (Bluetooth LE)
* nutze beim Radfahren deine Trittfrequenz- und Geschwindigkeitssensoren (Bluetooth LE)
* exportiert deine Daten als KML oder GPX
* funktioniert ganz ohne Internet
@@ -0,0 +1,4 @@
v3.6.1: OpenTracks
Bugfix:
- KMZ import: store correct picture path
@@ -0,0 +1,4 @@
v3.6.2: OpenTracks
Bugfix:
- Charts were not shown correctly (introduced in v3.6.0)
@@ -0,0 +1,13 @@
v3.7.0: OpenTracks
Changes:
- Report speed/pace by activity type
- Added workaround for GPS week rollover
- (Experimental) Support for Bluetooth LE cycling sensors (cadence and speed)
- KMZ/KML import enhanced
Bugfix:
- KMZ import: only import used images
Developer:
- Refactored TrackRecordingService
@@ -0,0 +1,7 @@
v3.7.1: OpenTracks
Workaround:
- Hide start GPS button
Bugfix:
- Fixed null pointer
@@ -0,0 +1,6 @@
v3.7.2: OpenTracks
Bugfix:
- Re-added start GPS button
- Track name is not overwritten at the end of a recording
- Markers: removing a photo also deletes the file
@@ -3,6 +3,8 @@ OpenTracks is a sport tracking application that completely respects your privacy
Features:
* Sport tracking: track your sport and outdoor activities
* Photos and markers: mark interesting locations while tracking
* Heart rate: supports Bluetooth LE sensors
* Supports Bluetooth LE sensors:
* heart rate
* cycling: speed and cadence
* Export data: export tracks either as KML or GPX
* No Internet access: Internet is not used
+2 -1
View File
@@ -1,5 +1,6 @@
#Sun Apr 12 18:41:12 CEST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -18,9 +18,7 @@ package de.dennisguse.opentracks;
import android.location.Location;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
/**
* Commons utilities for creating stubs of track, location.
@@ -37,23 +35,6 @@ public class TrackStubUtils {
private static final float INITIAL_ACCURACY = 5;
private static final float INITIAL_SPEED = 10;
private static final float INITIAL_BEARING = 3.0f;
// Used to change the value of latitude, longitude, and altitude.
private static final double DIFFERENCE = 0.01;
/**
* Gets a a {@link Track} stub with specified number of locations.
*
* @param numberOfLocations the number of locations for the track
* @return a track stub.
*/
public static Track createTrack(int numberOfLocations) {
Track track = new Track();
for (int i = 0; i < numberOfLocations; i++) {
track.addTrackPoint(createDefaultTrackPoint(INITIAL_LATITUDE + i * DIFFERENCE, INITIAL_LONGITUDE + i * DIFFERENCE, INITIAL_ALTITUDE + i * DIFFERENCE));
}
return track;
}
/**
* Create a MyTracks location with default values.
@@ -79,6 +60,6 @@ public class TrackStubUtils {
location.setTime(INITIAL_TIME);
location.setBearing(INITIAL_BEARING);
return new TrackPoint(location, new SensorDataSet(Float.NaN, Float.NaN));
return new TrackPoint(location);
}
}
@@ -88,6 +88,40 @@ public class DescriptionGeneratorTest {
Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
@Test
public void testGenerateTrackDescriptionWithoutMaxMinElevation() {
Track track = new Track();
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalTime(600000);
stats.setMovingTime(300000);
stats.setMaxSpeed(100);
stats.setMaxElevation(Double.POSITIVE_INFINITY);
stats.setMinElevation(Double.NEGATIVE_INFINITY);
stats.setTotalElevationGain(6000);
stats.setStartTime_ms(START_TIME);
track.setTrackStatistics(stats);
track.setCategory("hiking");
String expected = //"Created by"
"<a href='https://github.com/OpenTracksApp/OpenTracks'>OpenTracks</a><p>"
+ "Name: -<br>"
+ "Activity type: hiking<br>"
+ "Description: -<br>"
+ "Total distance: 20.00 km (12.4 mi)<br>"
+ "Total time: 10:00<br>"
+ "Moving time: 05:00<br>"
+ "Average speed: 120.00 km/h (74.6 mi/h)<br>"
+ "Average moving speed: 240.00 km/h (149.1 mi/h)<br>"
+ "Max speed: 360.00 km/h (223.7 mi/h)<br>"
+ "Average pace: 0:30 min/km (0:48 min/mi)<br>"
+ "Average moving pace: 0:15 min/km (0:24 min/mi)<br>"
+ "Fastest pace: 0:10 min/km (0:16 min/mi)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "<br>";
Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
/**
* Tests {@link DescriptionGenerator#writeDistance(double, StringBuilder, int, String)}.
@@ -70,7 +70,7 @@ public class SearchEngineTest {
providerUtils.deleteAllTracks(context);
}
private long insertTrack(String title, String description, String category, double distance, long hoursAgo) {
private long insertTrack(String title, String description, String category, long hoursAgo) {
Track track = new Track();
track.setName(title);
track.setDescription(description);
@@ -88,15 +88,11 @@ public class SearchEngineTest {
}
private long insertTrack(String title, String description, String category) {
return insertTrack(title, description, category, 0, -1);
}
private long insertTrack(String title, double distance) {
return insertTrack(title, "", "", distance, -1);
return insertTrack(title, description, category, -1);
}
private long insertTrack(String title, long hoursAgo) {
return insertTrack(title, "", "", 0.0, hoursAgo);
return insertTrack(title, "", "", hoursAgo);
}
private long insertWaypoint(String title, String description, String category, double distance, long hoursAgo, long trackId) {
@@ -1,6 +1,15 @@
package de.dennisguse.opentracks.content.data;
import android.content.Context;
import android.location.Location;
import android.net.Uri;
import android.util.Pair;
import java.io.File;
import java.io.IOException;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.util.FileUtils;
public class TestDataUtil {
@@ -8,21 +17,40 @@ public class TestDataUtil {
public static final double INITIAL_LONGITUDE = -57.0;
public static final double ALTITUDE_INTERVAL = 2.5;
/**
* Create a track without any trackPoints.
*/
public static Track createTrack(long trackId) {
Track track = new Track();
track.setId(trackId);
track.setName("Test: " + trackId);
return track;
}
/**
* Simulates a track which is used for testing.
*
* @param id the id of the track
* @param numPoints the location number in the track
* @return the simulated track
* @param trackId the trackId of the track
* @param numPoints the trackPoints number in the track
*/
public static Track getTrack(long id, int numPoints) {
Track track = new Track();
track.setId(id);
track.setName("Test: " + id);
public static Pair<Track, TrackPoint[]> createTrack(long trackId, int numPoints) {
Track track = createTrack(trackId);
TrackPoint[] trackPoints = new TrackPoint[numPoints];
for (int i = 0; i < numPoints; i++) {
track.addTrackPoint(createTrackPoint(i));
trackPoints[i] = (createTrackPoint(i));
}
return track;
return new Pair<>(track, trackPoints);
}
public static Track createTrackAndInsert(ContentProviderUtils contentProviderUtils, long trackId, int numPoints) {
Pair<Track, TrackPoint[]> pair = createTrack(trackId, numPoints);
insertTrackWithLocations(contentProviderUtils, pair.first, pair.second);
return pair.first;
}
/**
@@ -40,4 +68,33 @@ public class TestDataUtil {
location.setTime(i + 1);
return new TrackPoint(location);
}
/**
* Inserts a track with locations into the database.
*
* @param track track to be inserted
* @param trackPoints trackPoints to be inserted
*/
public static void insertTrackWithLocations(ContentProviderUtils contentProviderUtils, Track track, TrackPoint[] trackPoints) {
contentProviderUtils.insertTrack(track);
contentProviderUtils.bulkInsertTrackPoint(trackPoints, track.getId());
}
/**
* Creates a Waypoint with a photo.
*
* @param context The context.
* @param trackId The track id.
* @param location The location.
* @return the Waypoint created.
*/
public static Waypoint createWaypointWithPhoto(Context context, long trackId, Location location) throws IOException {
String photoUrl = "";
File dstFile = new File(FileUtils.getImageUrl(context, trackId));
dstFile.createNewFile();
Uri photoUri = FileUtils.getUriForFile(context, dstFile);
photoUrl = photoUri.toString();
return new Waypoint("Waypoint name", "Waypoint description", "Waypoint category", "", trackId, 0.0, 0, location, photoUrl);
}
}
@@ -21,6 +21,7 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
@@ -31,7 +32,10 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import de.dennisguse.opentracks.content.data.TestDataUtil;
@@ -42,6 +46,7 @@ import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.FileUtils;
import static org.mockito.Mockito.when;
@@ -57,6 +62,7 @@ public class CustomContentProviderUtilsTest {
private static final String MOCK_DESC = "Mock Next Waypoint Desc!";
private static final String TEST_DESC = "Test Desc!";
private static final String TEST_DESC_NEW = "Test Desc new!";
private static final String TEST_NAME_NEW = "Test Name new!";
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
@@ -141,7 +147,7 @@ public class CustomContentProviderUtilsTest {
loc.setAltitude(i * 2.5);
trackPoints[i] = new TrackPoint(loc);
}
contentProviderUtils.bulkInsertTrackPoint(trackPoints, numPoints, id);
contentProviderUtils.bulkInsertTrackPoint(trackPoints, id);
// Load all inserted trackPoints.
long lastPointId = -1;
@@ -194,10 +200,11 @@ public class CustomContentProviderUtilsTest {
public void testDeleteAllTracks() {
// Insert track, points and waypoint at first.
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
Waypoint waypoint = new Waypoint();
Track track = TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
Waypoint waypoint = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
contentProviderUtils.insertWaypoint(waypoint);
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
Assert.assertEquals(1, tracksCursor.getCount());
@@ -216,20 +223,58 @@ public class CustomContentProviderUtilsTest {
Assert.assertEquals(0, waypointCursor.getCount());
}
/**
* Tests the method {@link ContentProviderUtils#deleteAllTracks(Context)}
*/
@Test
public void testDeleteAllTracks_withWaypointAndPhoto() throws IOException {
// Insert track, points and waypoint with photo at first.
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
Waypoint waypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
contentProviderUtils.insertWaypoint(waypoint);
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
Assert.assertEquals(1, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
Assert.assertEquals(10, tracksPointsCursor.getCount());
Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
Assert.assertEquals(1, waypointCursor.getCount());
// Check waypoint has photo and it's in the external storage.
Assert.assertTrue(waypoint.hasPhoto());
File dir = FileUtils.getPhotoDir(context, trackId);
Assert.assertTrue(dir.isDirectory());
Assert.assertEquals(1, dir.list().length);
Assert.assertTrue(dir.exists());
// Delete all.
contentProviderUtils.deleteAllTracks(context);
// Check whether all have been deleted.
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
Assert.assertEquals(0, tracksCursor.getCount());
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
Assert.assertEquals(0, tracksPointsCursor.getCount());
waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
Assert.assertEquals(0, waypointCursor.getCount());
Assert.assertFalse(dir.exists());
}
/**
* Tests the method {@link ContentProviderUtils#deleteTrack(Context, long)}.
*/
@Test
public void testDeleteTrack() {
// Insert three tracks, points of two tracks and way point of one track.
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 0);
contentProviderUtils.insertTrack(track);
insertTrackWithLocations(TestDataUtil.getTrack(trackId + 1, 10));
insertTrackWithLocations(TestDataUtil.getTrack(trackId + 2, 10));
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId + 1, 10);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId + 2, 10);
Waypoint waypoint = new Waypoint();
Waypoint waypoint = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId + 1));
waypoint.setTrackId(trackId);
contentProviderUtils.insertWaypoint(waypoint);
@@ -251,15 +296,70 @@ public class CustomContentProviderUtilsTest {
Assert.assertEquals(0, waypointCursor.getCount());
}
/**
* Tests the method {@link ContentProviderUtils#deleteTrack(Context, long)}.
*/
@Test
public void testDeleteTrack_withWaypointPhoto() throws IOException {
// Insert three tracks.
long trackId = System.currentTimeMillis();
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId + 1, 10);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId + 2, 10);
// Insert a waypoint in tracks trackId and trackId + 1.
TrackPoint trackPoint1 = contentProviderUtils.getLastValidTrackPoint(trackId);
Waypoint waypoint1 = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint1.getLocation());
contentProviderUtils.insertWaypoint(waypoint1);
File dir1 = FileUtils.getPhotoDir(context, trackId);
TrackPoint trackPoint2 = contentProviderUtils.getLastValidTrackPoint(trackId + 1);
Waypoint waypoint2 = TestDataUtil.createWaypointWithPhoto(context, trackId + 1, trackPoint2.getLocation());
contentProviderUtils.insertWaypoint(waypoint2);
File dir2 = FileUtils.getPhotoDir(context, trackId + 1);
// Check.
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
Assert.assertEquals(3, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
Assert.assertEquals(30, tracksPointsCursor.getCount());
Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
Assert.assertEquals(2, waypointCursor.getCount());
Assert.assertTrue(waypoint1.hasPhoto());
Assert.assertTrue(dir1.isDirectory());
Assert.assertEquals(1, dir1.list().length);
Assert.assertTrue(dir1.exists());
Assert.assertTrue(dir2.isDirectory());
Assert.assertEquals(1, dir2.list().length);
Assert.assertTrue(dir2.exists());
// Delete one track.
contentProviderUtils.deleteTrack(context, trackId);
// Check whether all data of a track has been deleted.
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
Assert.assertEquals(2, tracksCursor.getCount());
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
Assert.assertEquals(20, tracksPointsCursor.getCount());
waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
Assert.assertEquals(1, waypointCursor.getCount());
Assert.assertFalse(dir1.exists());
Assert.assertTrue(dir2.exists());
}
/**
* Tests the method {@link ContentProviderUtils#getAllTracks()}
*/
@Test
public void testGetAllTracks() {
// given
int initialTrackNumber = contentProviderUtils.getAllTracks().size();
long trackId = System.currentTimeMillis();
contentProviderUtils.insertTrack(TestDataUtil.getTrack(trackId, 0));
contentProviderUtils.insertTrack(TestDataUtil.createTrack(trackId));
// when
List<Track> allTracks = contentProviderUtils.getAllTracks();
// then
Assert.assertEquals(initialTrackNumber + 1, allTracks.size());
Assert.assertEquals(trackId, allTracks.get(allTracks.size() - 1).getId());
}
@@ -270,7 +370,7 @@ public class CustomContentProviderUtilsTest {
@Test
public void testGetLastTrack() {
long trackId = System.currentTimeMillis();
contentProviderUtils.insertTrack(TestDataUtil.getTrack(trackId, 0));
contentProviderUtils.insertTrack(TestDataUtil.createTrack(trackId));
Assert.assertEquals(trackId, contentProviderUtils.getLastTrack().getId());
}
@@ -280,7 +380,7 @@ public class CustomContentProviderUtilsTest {
@Test
public void testGetTrack() {
long trackId = System.currentTimeMillis();
contentProviderUtils.insertTrack(TestDataUtil.getTrack(trackId, 0));
contentProviderUtils.insertTrack(TestDataUtil.createTrack(trackId));
Assert.assertNotNull(contentProviderUtils.getTrack(trackId));
}
@@ -289,11 +389,14 @@ public class CustomContentProviderUtilsTest {
*/
@Test
public void testUpdateTrack() {
// given
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 0);
Track track = TestDataUtil.createTrack(trackId);
String nameOld = "name1";
String nameNew = "name2";
track.setName(nameOld);
// when / then
contentProviderUtils.insertTrack(track);
Assert.assertEquals(nameOld, contentProviderUtils.getTrack(trackId).getName());
track.setName(nameNew);
@@ -307,7 +410,8 @@ public class CustomContentProviderUtilsTest {
@Test
public void testCreateContentValues_waypoint() {
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
// Bottom
long startTime = 1000L;
// AverageSpeed
@@ -322,17 +426,10 @@ public class CustomContentProviderUtilsTest {
statistics.setMaxElevation(1250.0);
statistics.setMinElevation(1200.0);
track.setTrackStatistics(statistics);
contentProviderUtils.insertTrack(track);
track.first.setTrackStatistics(statistics);
contentProviderUtils.insertTrack(track.first);
// Insert at first.
Location location = new Location("test");
location.setLatitude(22);
location.setLongitude(22);
location.setAccuracy((float) 1 / 100.0f);
location.setAltitude(2.5);
Waypoint waypoint = new Waypoint(location);
Waypoint waypoint = new Waypoint(track.second[0]);
waypoint.setDescription(TEST_DESC);
contentProviderUtils.insertWaypoint(waypoint);
@@ -342,7 +439,7 @@ public class CustomContentProviderUtilsTest {
waypoint.setId(waypointId);
ContentValues contentValues = contentProviderUtils.createContentValues(waypoint);
Assert.assertEquals(waypointId, contentValues.get(WaypointsColumns._ID));
Assert.assertEquals(22 * 1000000, contentValues.get(WaypointsColumns.LONGITUDE));
Assert.assertEquals((int) (TestDataUtil.INITIAL_LONGITUDE * 1000000), contentValues.get(WaypointsColumns.LONGITUDE));
Assert.assertEquals(TEST_DESC, contentValues.get(WaypointsColumns.DESCRIPTION));
}
@@ -382,66 +479,106 @@ public class CustomContentProviderUtilsTest {
/**
* Tests the method
* {@link ContentProviderUtils#deleteWaypoint(long)}
* {@link ContentProviderUtils#deleteWaypoint(Context, long)}
* when there is only one waypoint in the track.
*/
@Test
public void testDeleteWaypoint_onlyOneWayPoint() {
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
// Insert at first.
Waypoint waypoint1 = new Waypoint();
Waypoint waypoint1 = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint1.setDescription(TEST_DESC);
waypoint1.setTrackId(trackId);
contentProviderUtils.insertWaypoint(waypoint1);
// Delete
contentProviderUtils.deleteWaypoint(1);
// Check insert was done.
Assert.assertEquals(contentProviderUtils.getWaypointCount(trackId), 1);
Assert.assertNull(contentProviderUtils.getWaypoint(1));
// Get waypoint id that needs to delete.
long waypoint1Id = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint1));
// Delete
contentProviderUtils.deleteWaypoint(context, waypoint1Id);
Assert.assertNull(contentProviderUtils.getWaypoint(waypoint1Id));
}
/**
* Tests the method
* {@link ContentProviderUtils#deleteWaypoint(long)}
* when there is more than one waypoint in the track.
* {@link ContentProviderUtils#deleteWaypoint(Context, long)}
* when there is only one waypoint in the track.
*/
@Test
public void testDeleteWaypoint_onlyOneWayPointWithPhotoUrl() throws IOException {
long trackId = System.currentTimeMillis();
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
// Insert at first.
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
Waypoint waypoint1 = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
contentProviderUtils.insertWaypoint(waypoint1);
// Check insert was done.
Assert.assertEquals(contentProviderUtils.getWaypointCount(trackId), 1);
// Get waypoint id that needs to delete.
long waypoint1Id = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint1));
// Check waypoint has photo and it's in the external storage.
Assert.assertTrue(waypoint1.hasPhoto());
File dir = FileUtils.getPhotoDir(context, trackId);
Assert.assertTrue(dir.isDirectory());
Assert.assertEquals(1, dir.list().length);
Assert.assertTrue(dir.exists());
// Delete
contentProviderUtils.deleteWaypoint(context, waypoint1Id);
// Check waypoint doesn't exists and photo folder was deleted.
Assert.assertNull(contentProviderUtils.getWaypoint(waypoint1Id));
Assert.assertFalse(dir.exists());
}
/**
* Tests the method {@link ContentProviderUtils#deleteWaypoint(Context, long)} when there is more than one waypoint in the track.
*/
@Test
public void testDeleteWaypoint_hasNextWayPoint() {
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
TrackStatistics statistics = new TrackStatistics();
statistics.setStartTime_ms(1000L);
statistics.setStopTime_ms(2500L);
statistics.setTotalTime(1500L);
statistics.setMovingTime(700L);
statistics.setTotalDistance(750.0);
statistics.setTotalElevationGain(50.0);
statistics.setMaxSpeed(60.0);
statistics.setMaxElevation(1250.0);
statistics.setMinElevation(1200.0);
track.setTrackStatistics(statistics);
contentProviderUtils.insertTrack(track);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
// Track track = TestDataUtil.createTrackAndInsert(trackId, 10);
//
// TrackStatistics statistics = new TrackStatistics();
// statistics.setStartTime_ms(1000L);
// statistics.setStopTime_ms(2500L);
// statistics.setTotalTime(1500L);
// statistics.setMovingTime(700L);
// statistics.setTotalDistance(750.0);
// statistics.setTotalElevationGain(50.0);
// statistics.setMaxSpeed(60.0);
// statistics.setMaxElevation(1250.0);
// statistics.setMinElevation(1200.0);
//
// track.setTrackStatistics(statistics);
// TestDataUtil.insertTrackWithLocations(contentProviderUtils, track);
// Insert at first.
Waypoint waypoint1 = new Waypoint();
Waypoint waypoint1 = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint1.setDescription(MOCK_DESC);
waypoint1.setTrackId(trackId);
long waypoint1Id = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint1));
Waypoint waypoint2 = new Waypoint();
Waypoint waypoint2 = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint2.setDescription(MOCK_DESC);
waypoint2.setTrackId(trackId);
long waypoint2Id = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint2));
// Delete
Assert.assertNotNull(contentProviderUtils.getWaypoint(waypoint1Id));
contentProviderUtils.deleteWaypoint(waypoint1Id);
contentProviderUtils.deleteWaypoint(context, waypoint1Id);
Assert.assertNull(contentProviderUtils.getWaypoint(waypoint1Id));
Assert.assertEquals(MOCK_DESC, contentProviderUtils.getWaypoint(waypoint2Id).getDescription());
@@ -453,16 +590,15 @@ public class CustomContentProviderUtilsTest {
@Test
public void testGetNextWaypointNumber() {
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
Waypoint waypoint1 = new Waypoint();
Waypoint waypoint1 = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint1.setTrackId(trackId);
Waypoint waypoint2 = new Waypoint();
Waypoint waypoint2 = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint2.setTrackId(trackId);
Waypoint waypoint3 = new Waypoint();
Waypoint waypoint3 = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint3.setTrackId(trackId);
Waypoint waypoint4 = new Waypoint();
Waypoint waypoint4 = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint4.setTrackId(trackId);
contentProviderUtils.insertWaypoint(waypoint1);
contentProviderUtils.insertWaypoint(waypoint2);
@@ -479,10 +615,9 @@ public class CustomContentProviderUtilsTest {
@Test
public void testInsertAndGetWaypoint() {
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
Waypoint waypoint = new Waypoint();
Waypoint waypoint = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint.setDescription(TEST_DESC);
waypoint.setTrackId(trackId);
long waypointId = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint));
@@ -491,15 +626,15 @@ public class CustomContentProviderUtilsTest {
}
/**
* Tests the method {@link ContentProviderUtils#updateWaypoint(Waypoint)}.
* Tests the method {@link ContentProviderUtils#updateWaypoint(Context, Waypoint)}.
*/
@Test
public void testUpdateWaypoint() {
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
// Insert at first.
Waypoint waypoint = new Waypoint();
Waypoint waypoint = new Waypoint(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint.setDescription(TEST_DESC);
waypoint.setTrackId(trackId);
long waypointId = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint));
@@ -507,84 +642,178 @@ public class CustomContentProviderUtilsTest {
// Update
waypoint = contentProviderUtils.getWaypoint(waypointId);
waypoint.setDescription(TEST_DESC_NEW);
contentProviderUtils.updateWaypoint(waypoint);
contentProviderUtils.updateWaypoint(context, waypoint);
Assert.assertEquals(TEST_DESC_NEW, contentProviderUtils.getWaypoint(waypointId).getDescription());
}
/**
* Tests the method {@link ContentProviderUtils#bulkInsertTrackPoint(TrackPoint[], int, long)}.
* Tests the method {@link ContentProviderUtils#updateWaypoint(Context, Waypoint)}.
*/
@Test
public void testUpdateWaypoint_withPhoto() throws IOException {
// tests after update waypoint with photo the photo remains in the storage.
long trackId = System.currentTimeMillis();
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
// Insert at first.
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
Waypoint waypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
waypoint.setDescription(TEST_DESC);
waypoint.setTrackId(trackId);
long waypointId = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint));
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId);
Assert.assertTrue(dir.exists());
Assert.assertTrue(dir.isDirectory());
Assert.assertEquals(1, dir.list().length);
// Update
waypoint = contentProviderUtils.getWaypoint(waypointId);
waypoint.setName(TEST_NAME_NEW);
waypoint.setDescription(TEST_DESC_NEW);
contentProviderUtils.updateWaypoint(context, waypoint);
Assert.assertEquals(TEST_NAME_NEW, contentProviderUtils.getWaypoint(waypointId).getName());
Assert.assertEquals(TEST_DESC_NEW, contentProviderUtils.getWaypoint(waypointId).getDescription());
Assert.assertTrue(waypoint.hasPhoto());
Assert.assertTrue(dir.exists());
Assert.assertTrue(dir.isDirectory());
Assert.assertEquals(1, dir.list().length);
}
/**
* Tests the method {@link ContentProviderUtils#updateWaypoint(Context, Waypoint)}.
*/
@Test
public void testUpdateWaypoint_delPhotoAndDir() throws IOException {
// tests after update waypoint if user deletes the photo then file photo is deleted from the storage. Also empty directory is deleted.
long trackId = System.currentTimeMillis();
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
// Insert at first.
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
Waypoint waypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
waypoint.setDescription(TEST_DESC);
waypoint.setTrackId(trackId);
long waypointId = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint));
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId);
Assert.assertTrue(dir.exists());
Assert.assertTrue(dir.isDirectory());
Assert.assertEquals(1, dir.list().length);
// Update
waypoint = contentProviderUtils.getWaypoint(waypointId);
waypoint.setName(TEST_NAME_NEW);
waypoint.setDescription(TEST_DESC_NEW);
waypoint.setPhotoUrl(null);
contentProviderUtils.updateWaypoint(context, waypoint);
Assert.assertEquals(TEST_NAME_NEW, contentProviderUtils.getWaypoint(waypointId).getName());
Assert.assertEquals(TEST_DESC_NEW, contentProviderUtils.getWaypoint(waypointId).getDescription());
Assert.assertFalse(waypoint.hasPhoto());
Assert.assertFalse(dir.exists());
}
/**
* Tests the method {@link ContentProviderUtils#updateWaypoint(Context, Waypoint)}.
*/
@Test
public void testUpdateWaypoint_delPhotoNotDir() throws IOException {
// tests after update waypoint if user deletes the photo then file photo is deleted from the storage. Directory remains if there are more photos from other waypoints.
long trackId = System.currentTimeMillis();
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
// Insert two waypoints with photos.
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
Waypoint waypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
waypoint.setDescription(TEST_DESC);
waypoint.setTrackId(trackId);
Waypoint otherWaypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
otherWaypoint.setDescription(TEST_DESC);
otherWaypoint.setTrackId(trackId);
long waypointId = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint));
long otherWaypointId = ContentUris.parseId(contentProviderUtils.insertWaypoint(otherWaypoint));
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId);
Assert.assertTrue(dir.exists());
Assert.assertTrue(dir.isDirectory());
Assert.assertEquals(2, dir.list().length);
// Update one waypoint deleting photo.
waypoint = contentProviderUtils.getWaypoint(waypointId);
waypoint.setPhotoUrl(null);
contentProviderUtils.updateWaypoint(context, waypoint);
Assert.assertEquals(TEST_DESC, contentProviderUtils.getWaypoint(waypointId).getDescription());
Assert.assertFalse(waypoint.hasPhoto());
Assert.assertTrue(dir.exists());
Assert.assertTrue(dir.isDirectory());
Assert.assertEquals(1, dir.list().length);
}
/**
* Tests the method {@link ContentProviderUtils#bulkInsertTrackPoint(TrackPoint[], long)}.
*/
@Test
public void testBulkInsertTrackPoint() {
// Insert track, point at first.
// given
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track.first, track.second);
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), -1, trackId);
// when / then
contentProviderUtils.bulkInsertTrackPoint(track.second, trackId);
Assert.assertEquals(20, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), 8, trackId);
contentProviderUtils.bulkInsertTrackPoint(Arrays.copyOfRange(track.second, 0, 8), trackId);
Assert.assertEquals(28, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
}
/**
* Tests the method {@link ContentProviderUtils#createTrackPoint(Cursor)}.
*/
//TODO incomplete
@Test
public void testCreateTrackPoint() {
// Set index.
int index = 1;
// Id
when(cursorMock.getColumnIndex(TrackPointsColumns._ID)).thenReturn(index++);
// Longitude
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE)).thenReturn(index++);
// Latitude
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE)).thenReturn(index++);
// Time
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.TIME))
.thenReturn(index++);
// Speed
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SPEED)).thenReturn(index++);
// Sensor
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE)).thenReturn(index++);
// given
when(cursorMock.getColumnIndex(TrackPointsColumns._ID)).thenReturn(1);
// Set return value of isNull().
index = 2;
// Longitude
when(cursorMock.isNull(index++)).thenReturn(false);
// Latitude
when(cursorMock.isNull(index++)).thenReturn(false);
// Time
when(cursorMock.isNull(index++)).thenReturn(false);
// Speed
when(cursorMock.isNull(index++)).thenReturn(false);
// Sensor
when(cursorMock.isNull(index++)).thenReturn(false);
// Set return value of isNull().
index = 2;
// Longitude
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE)).thenReturn(2);
when(cursorMock.isNull(2)).thenReturn(false);
int longitude = 11;
when(cursorMock.getInt(index++)).thenReturn(longitude * 1000000);
// Latitude.
int latitude = 22;
when(cursorMock.getInt(index++)).thenReturn(latitude * 1000000);
// Time
long time = System.currentTimeMillis();
when(cursorMock.getLong(index++)).thenReturn(time);
// Speed
float speed = 2.2f;
when(cursorMock.getFloat(index++)).thenReturn(speed);
// Sensor
byte[] sensor = "Sensor state".getBytes();
when(cursorMock.getBlob(index++)).thenReturn(sensor);
when(cursorMock.getInt(2)).thenReturn(longitude * 1000000);
TrackPoint location = contentProviderUtils.createTrackPoint(cursorMock);
Assert.assertEquals(longitude, location.getLongitude(), 0.01);
Assert.assertEquals(latitude, location.getLatitude(), 0.01);
Assert.assertEquals(time, location.getTime(), 0.01);
Assert.assertEquals(speed, location.getSpeed(), 0.01);
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE)).thenReturn(3);
when(cursorMock.isNull(3)).thenReturn(false);
int latitude = 22;
when(cursorMock.getInt(3)).thenReturn(latitude * 1000000);
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.TIME)).thenReturn(4);
when(cursorMock.isNull(4)).thenReturn(false);
long time = System.currentTimeMillis();
when(cursorMock.getLong(4)).thenReturn(time);
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SPEED)).thenReturn(5);
when(cursorMock.isNull(5)).thenReturn(false);
float speed = 2.2f;
when(cursorMock.getFloat(5)).thenReturn(speed);
when(cursorMock.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE)).thenReturn(6);
when(cursorMock.isNull(6)).thenReturn(false);
// when
TrackPoint trackPoint = contentProviderUtils.createTrackPoint(cursorMock);
// then
Assert.assertEquals(longitude, trackPoint.getLongitude(), 0.01);
Assert.assertEquals(latitude, trackPoint.getLatitude(), 0.01);
Assert.assertEquals(time, trackPoint.getTime(), 0.01);
Assert.assertEquals(speed, trackPoint.getSpeed(), 0.01);
Assert.assertFalse(trackPoint.hasHeartRate());
}
/**
@@ -595,11 +824,32 @@ public class CustomContentProviderUtilsTest {
public void testInsertTrackPoint() {
// Insert track, point at first.
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
Track track = TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
contentProviderUtils.insertTrackPoint(TestDataUtil.createTrackPoint(22), trackId);
Assert.assertEquals(11, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
Assert.assertEquals(11, contentProviderUtils.getTrackPoints(trackId).size());
}
@Test
public void testInsertAndLoadTrackPoint() {
// given
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
TrackPoint trackPoint = TestDataUtil.createTrackPoint(5);
trackPoint.setHeartRate_bpm(1F);
trackPoint.setCyclingCadence_rpm(2F);
trackPoint.setPower(3F);
// when
contentProviderUtils.insertTrackPoint(trackPoint, trackId);
// then
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
Assert.assertTrue(trackPoints.get(10).hasHeartRate());
Assert.assertEquals(trackPoint.getHeartRate_bpm(), trackPoints.get(10).getHeartRate_bpm(), 0.01);
Assert.assertEquals(trackPoint.getCyclingCadence_rpm(), trackPoints.get(10).getCyclingCadence_rpm(), 0.01);
Assert.assertEquals(trackPoint.getPower(), trackPoints.get(10).getPower(), 0.01);
}
/**
@@ -609,8 +859,7 @@ public class CustomContentProviderUtilsTest {
public void testGetLastValidTrackPoint() {
// Insert track, points at first.
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
Track track = TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
TrackPoint lastTrackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
checkLocation(9, lastTrackPoint.getLocation());
@@ -621,17 +870,20 @@ public class CustomContentProviderUtilsTest {
*/
@Test
public void testGetTrackPointCursor_desc() {
// Insert track, points at first.
// given
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
contentProviderUtils.insertTrack(track.first);
long[] trackpointIds = new long[track.getTrackPoints().size()];
long[] trackpointIds = new long[track.second.length];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.second[i], track.first.getId()));
}
// when
Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds[1], 5, true);
// then
Assert.assertEquals(2, cursor.getCount());
}
@@ -640,17 +892,20 @@ public class CustomContentProviderUtilsTest {
*/
@Test
public void testGetTrackPointCursor_asc() {
// Insert track, points at first.
// given
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
contentProviderUtils.insertTrack(track.first);
long[] trackpointIds = new long[track.getTrackPoints().size()];
long[] trackpointIds = new long[track.second.length];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.second[i], track.first.getId()));
}
// when
Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds[8], 5, false);
// then
Assert.assertEquals(2, cursor.getCount());
}
@@ -659,19 +914,21 @@ public class CustomContentProviderUtilsTest {
*/
@Test
public void testGetTrackPointLocationIterator_desc() {
// Insert track, points at first.
// given
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
contentProviderUtils.insertTrack(track.first);
long[] trackpointIds = new long[track.getTrackPoints().size()];
long[] trackpointIds = new long[track.second.length];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.second[i], track.first.getId()));
}
long startTrackPointId = trackpointIds[9];
// when
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true);
// then
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(trackPointIterator.hasNext());
TrackPoint trackPoint = trackPointIterator.next();
@@ -686,33 +943,36 @@ public class CustomContentProviderUtilsTest {
*/
@Test
public void testGetTrackPointLocationIterator_asc() {
// Insert track, point at first.
// given
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
contentProviderUtils.insertTrack(track.first);
long[] trackpointIds = new long[track.getTrackPoints().size()];
long[] trackpointIds = new long[track.second.length];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.second[i], track.first.getId()));
}
long startTrackPointId = trackpointIds[0];
TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false);
// when
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false);
// then
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext());
TrackPoint trackPoint = locationIterator.next();
Assert.assertEquals(startTrackPointId + i, locationIterator.getTrackPointId());
Assert.assertTrue(trackPointIterator.hasNext());
TrackPoint trackPoint = trackPointIterator.next();
Assert.assertEquals(startTrackPointId + i, trackPointIterator.getTrackPointId());
checkLocation(i, trackPoint.getLocation());
}
Assert.assertFalse(locationIterator.hasNext());
Assert.assertFalse(trackPointIterator.hasNext());
}
/**
* Checks the value of a location.
*
* @param i the index of this location which created in the method {@link TestDataUtil#getTrack(long, int)}
* @param i the index of this location which created in the method {@link TestDataUtil#createTrack(long, int)}
* @param location the location to be checked
*/
private void checkLocation(int i, Location location) {
@@ -722,15 +982,6 @@ public class CustomContentProviderUtilsTest {
Assert.assertEquals(i * TestDataUtil.ALTITUDE_INTERVAL, location.getAltitude(), 0.01);
}
/**
* Inserts a track with locations into the database.
*
* @param track track to be inserted
*/
private void insertTrackWithLocations(Track track) {
contentProviderUtils.insertTrack(track);
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), track.getTrackPoints().size(), track.getId());
}
@Test
public void testFormatIdListForUri() {
@@ -0,0 +1,92 @@
package de.dennisguse.opentracks.content.sensor;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.util.UintUtils;
@RunWith(AndroidJUnit4.class)
public class SensorDataCyclingTest {
@Test
public void compute_cadence() {
// given
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 1, 1024); // 1s
SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 2, 2048); // 2s
// when
current.compute(previous);
// then
Assert.assertEquals(60, current.getCadence_rpm(), 0.01);
}
@Test
public void compute_cadence_sameCount() {
// given
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 1, 1024);
SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 1, 2048);
// when
current.compute(previous);
// then
Assert.assertEquals(0, current.getCadence_rpm(), 0.01);
}
@Test
public void compute_cadence_sameTime() {
// given
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 1, 1024);
SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 2, 1024);
// when
current.compute(previous);
// then
Assert.assertFalse(current.hasCadence_rpm());
}
@Test
public void compute_cadence_rollOverTime() {
// given
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 1, UintUtils.UINT16_MAX - 1024);
SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 2, 0);
// when
current.compute(previous);
// then
Assert.assertEquals(60, current.getCadence_rpm(), 0.01);
}
@Test
public void compute_cadence_rollOverCount() {
// given
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress", "sensorName", UintUtils.UINT32_MAX - 1, 1024);
SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 0, 2048);
// when
current.compute(previous);
// then
Assert.assertEquals(60, current.getCadence_rpm(), 0.01);
}
@Test
public void compute_speed_rollOverCount() {
// given
SensorDataCycling.Speed previous = new SensorDataCycling.Speed("sensorAddress", "sensorName", UintUtils.UINT16_MAX - 1, 1024);
SensorDataCycling.Speed current = new SensorDataCycling.Speed("sensorAddress", "sensorName", 0, 2048);
// when
current.compute(previous, 2000);
// then
Assert.assertEquals(2, current.getSpeed_mps(), 0.01);
}
}
@@ -33,7 +33,6 @@ import de.dennisguse.opentracks.TrackStubUtils;
import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.util.UnitConversions;
/**
@@ -72,20 +71,13 @@ public class ChartFragmentTest {
*/
@Test
public void testCreatePendingPoint_sensorIncorrect() {
// given
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
// No input.
// when
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01);
Assert.assertEquals(Float.NaN, point.getCadence(), 0.01);
Assert.assertEquals(Float.NaN, point.getPower(), 0.01);
// Input incorrect state.
// Creates SensorData.
SensorDataSet sensorDataSet = new SensorDataSet(SensorDataSet.DATA_UNAVAILABLE, SensorDataSet.DATA_UNAVAILABLE);
trackPoint.setSensorDataSet(sensorDataSet);
// Test.
point = chartFragment.createPendingPoint(trackPoint);
// then
Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01);
Assert.assertEquals(Float.NaN, point.getCadence(), 0.01);
Assert.assertEquals(Float.NaN, point.getPower(), 0.01);
@@ -96,20 +88,16 @@ public class ChartFragmentTest {
*/
@Test
public void testCreatePendingPoint_sensorCorrect() {
// given
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
// No input.
trackPoint.setHeartRate_bpm(100f);
trackPoint.setCyclingCadence_rpm(101f);
trackPoint.setPower(102f);
// when
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01);
Assert.assertEquals(Float.NaN, point.getCadence(), 0.01);
Assert.assertEquals(Float.NaN, point.getPower(), 0.01);
// Creates SensorData.
SensorDataSet sensorDataSet = new SensorDataSet(100, 101, 102);
// Creates SensorDataSet.
trackPoint.setSensorDataSet(sensorDataSet);
// Test.
point = chartFragment.createPendingPoint(trackPoint);
// then
Assert.assertEquals(100.0, point.getHeartRate(), 0.01);
Assert.assertEquals(101.0, point.getCadence(), 0.01);
Assert.assertEquals(102.0, point.getPower(), 0.01);
@@ -101,7 +101,7 @@ public abstract class AbstractTestFileTrackImporter {
* @param trackPointId the track point id
*/
protected void expectFirstTrackPoint(TrackPoint trackPoint, long trackId, long trackPointId) {
when(contentProviderUtils.bulkInsertTrackPoint(trackPoint != null ? (TrackPoint[]) any() : (TrackPoint[]) any(), eq(1), eq(trackId))).thenReturn(1);
when(contentProviderUtils.bulkInsertTrackPoint(trackPoint != null ? (TrackPoint[]) any() : (TrackPoint[]) any(), eq(trackId))).thenReturn(1);
}
/**
@@ -2,6 +2,7 @@ package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
@@ -53,15 +54,15 @@ public class ExportImportTest {
@Before
public void setUp() {
Track track = TestDataUtil.getTrack(trackId, 10);
track.setIcon(TRACK_ICON);
track.setCategory(TRACK_CATEGORY);
track.setDescription(TRACK_DESCRIPTION);
contentProviderUtils.insertTrack(track);
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), track.getTrackPoints().size(), track.getId());
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
track.first.setIcon(TRACK_ICON);
track.first.setCategory(TRACK_CATEGORY);
track.first.setDescription(TRACK_DESCRIPTION);
contentProviderUtils.insertTrack(track.first);
contentProviderUtils.bulkInsertTrackPoint(track.second, track.first.getId());
for (int i = 0; i < 3; i++) {
Waypoint waypoint = new Waypoint(track.getTrackPoints().get(i).getLocation());
Waypoint waypoint = new Waypoint(track.second[i].getLocation());
waypoint.setName("the waypoint " + i);
waypoint.setDescription("the waypoint description " + i);
waypoint.setCategory("the waypoint category" + i);
@@ -114,7 +115,7 @@ public class ExportImportTest {
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals(track.getTrackPoints(), importedTrack.getTrackPoints());
//TODO assertEquals(track.getTrackPoints(), importedTrack.getTrackPoints());
assertEquals(track.getCategory(), importedTrack.getCategory());
assertEquals(track.getDescription(), importedTrack.getDescription());
assertEquals(track.getName(), importedTrack.getName());
@@ -184,7 +185,7 @@ public class ExportImportTest {
// 1. track
Track trackImported = contentProviderUtils.getTrack(importTrackId);
assertNotNull(trackImported);
assertEquals(track.getTrackPoints(), trackImported.getTrackPoints());
//TODO assertEquals(track.getTrackPoints(), trackImported.getTrackPoints());
assertEquals(track.getCategory(), trackImported.getCategory());
assertEquals(track.getDescription(), trackImported.getDescription());
assertEquals(track.getName(), trackImported.getName());
@@ -30,7 +30,6 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq;
@@ -91,7 +90,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
@@ -120,7 +119,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -152,7 +151,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
expectFirstTrackPoint(null, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -206,7 +205,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
// For the following, use StubReturn since we don't care whether they are invoked or not.
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), anyInt(), anyLong())).thenReturn(1);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), anyLong())).thenReturn(1);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
contentProviderUtils.deleteTrack(context, TRACK_ID_0);
@@ -75,7 +75,7 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -106,7 +106,7 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -24,6 +24,7 @@ import android.os.IBinder;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.FlakyTest;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
import androidx.test.rule.GrantPermissionRule;
@@ -43,6 +44,8 @@ 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.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
@@ -86,7 +89,7 @@ public class TrackRecordingServiceTest {
// Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().apply();
sharedPreferences.edit().clear().commit();
// Ensure that the database is empty before every test
contentProviderUtils.deleteAllTracks(context);
@@ -121,13 +124,16 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testRecording_noTracks() throws Exception {
// given
List<Track> tracks = contentProviderUtils.getAllTracks();
Assert.assertTrue(tracks.isEmpty());
// when
Intent startIntent = createStartIntent(context);
mServiceRule.startService(startIntent);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(startIntent));
// then
// Test if we start in no-recording mode by default.
Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId());
@@ -136,19 +142,92 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testRecording_oldTracks() throws Exception {
// given
createDummyTrack(trackId, -1L, false);
// when
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
// then
Assert.assertFalse(service.isRecording());
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testRecording_orphanedRecordingTrack() throws Exception {
Intent startIntent = createStartIntent(context);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(startIntent));
public void testRecording_serviceRestart_whileRecording() throws Exception {
// given
createDummyTrack(trackId, -1L, true);
//when
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
// then
Assert.assertTrue(service.isRecording());
}
@MediumTest
@Test
public void testRecording_pauseAndResume() throws Exception {
// given
createDummyTrack(trackId, -1L, true);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
insertLocation(service);
// when
service.pauseCurrentTrack();
// then
Assert.assertEquals(2, contentProviderUtils.getTrackPoints(trackId).size());
//when
service.resumeTrack(trackId);
insertLocation(service);
// then
Assert.assertTrue(service.isRecording());
Assert.assertEquals(trackId, service.getRecordingTrackId());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
Assert.assertEquals(5, trackPoints.size());
Assert.assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
Assert.assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(2).getLatitude(), 0.01);
Assert.assertEquals(TrackPointsColumns.RESUME_LATITUDE, trackPoints.get(3).getLatitude(), 0.01);
}
@MediumTest
@Test
public void testRecording_resumeStoppedTrack() throws Exception {
// given
createDummyTrack(trackId, -1L, true);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
insertLocation(service);
service.endCurrentTrack();
Assert.assertEquals(1, contentProviderUtils.getTrackPoints(trackId).size());
// when
service.resumeTrack(trackId);
insertLocation(service);
// then
Assert.assertTrue(service.isRecording());
Assert.assertEquals(trackId, service.getRecordingTrackId());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
Assert.assertEquals(4, trackPoints.size());
Assert.assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
Assert.assertEquals(TrackPointsColumns.RESUME_LATITUDE, trackPoints.get(2).getLatitude(), 0.01);
}
@FlakyTest(detail = "Sometimes fails on CI.")
@MediumTest
@Test
public void testRecording_orphanedRecordingTrack() throws Exception {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
// when
// Just set recording track to a bogus value.
// Make sure that the service will not start recording and will clear the bogus track.
PreferencesUtils.setLong(context, R.string.recording_track_id_key, 123L);
@@ -161,33 +240,86 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testStartNewTrack_alreadyRecording() throws Exception {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
Assert.assertTrue(service.isRecording());
long trackId = service.getRecordingTrackId();
// when
long newTrackId = service.startNewTrack();
// then
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, newTrackId);
Assert.assertEquals(trackId, PreferencesUtils.getRecordingTrackId(context));
Assert.assertEquals(trackId, service.getRecordingTrackId());
service.endCurrentTrack();
}
@MediumTest
@Test
public void testEndCurrentTrack_noRecording() throws Exception {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
// when
// Ending the current track when there is no recording should not result in any error.
service.endCurrentTrack();
// then
Assert.assertFalse(PreferencesUtils.isRecording(context));
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
// when
long waypointId = service.insertWaypoint(null, null, null, null);
// then
Assert.assertEquals(-1L, waypointId);
}
@MediumTest
@Test
public void testInsertWaypointMarker_validWaypoint() throws Exception {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
Assert.assertTrue(service.isRecording());
insertLocation(service);
long trackId = service.getRecordingTrackId();
// when
long waypointId = service.insertWaypoint(null, null, null, null);
// then
Assert.assertNotEquals(-1L, waypointId);
Waypoint wpt = contentProviderUtils.getWaypoint(waypointId);
Assert.assertEquals(context.getString(R.string.marker_waypoint_icon_url), wpt.getIcon());
Assert.assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
Assert.assertEquals(trackId, wpt.getTrackId());
Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation());
service.endCurrentTrack();
}
private void addTrack(Track track, boolean isRecording) {
Assert.assertTrue(track.getId() >= 0);
contentProviderUtils.insertTrack(track);
Assert.assertEquals(track.getId(), contentProviderUtils.getTrack(track.getId()).getId());
PreferencesUtils.setLong(context, R.string.recording_track_id_key, isRecording ? track.getId() : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, !isRecording);
}
// NOTE: Do not use to create a track that is currently recording.
private void createDummyTrack(long id, long stopTime, boolean isRecording) {
Track dummyTrack = new Track();
@@ -199,58 +331,28 @@ public class TrackRecordingServiceTest {
addTrack(dummyTrack, isRecording);
}
private void addTrack(Track track, boolean isRecording) {
Assert.assertTrue(track.getId() >= 0);
contentProviderUtils.insertTrack(track);
Assert.assertEquals(track.getId(), contentProviderUtils.getTrack(track.getId()).getId());
PreferencesUtils.setLong(context, R.string.recording_track_id_key, isRecording ? track.getId() : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, !isRecording);
private static void insertLocation(TrackRecordingServiceInterface trackRecordingService) throws InterruptedException {
insertLocation(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());
}
/**
* Inserts a location and waits for 200ms.
*/
private void insertLocation(TrackRecordingServiceInterface trackRecordingService) throws InterruptedException {
private static void insertLocation(TrackRecordingServiceInterface trackRecordingService, double latitude, double longitude, float accuracy, long speed, long time) throws InterruptedException {
Location location = new Location("gps");
location.setLongitude(35.0f);
location.setLatitude(45.0f);
location.setAccuracy(5);
location.setSpeed(10);
location.setTime(System.currentTimeMillis());
location.setLongitude(longitude);
location.setLatitude(latitude);
location.setAccuracy(accuracy);
location.setSpeed(speed);
location.setTime(time);
location.setBearing(3.0f);
trackRecordingService.insertLocation(location);
//TODO Needed?
Thread.sleep(200);
}
@MediumTest
@Test
public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
long waypointId = service.insertWaypoint(null, null, null, null);
Assert.assertEquals(-1L, waypointId);
}
@MediumTest
@Test
public void testInsertWaypointMarker_validWaypoint() throws Exception {
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
Assert.assertTrue(service.isRecording());
insertLocation(service);
long trackId = service.getRecordingTrackId();
long waypointId = service.insertWaypoint(null, null, null, null);
Assert.assertNotEquals(-1L, waypointId);
Waypoint wpt = contentProviderUtils.getWaypoint(waypointId);
Assert.assertEquals(context.getString(R.string.marker_waypoint_icon_url), wpt.getIcon());
Assert.assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
Assert.assertEquals(trackId, wpt.getTrackId());
Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation());
service.endCurrentTrack();
}
}
@@ -0,0 +1,327 @@
package de.dennisguse.opentracks.services;
import android.content.ContentProvider;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* Tests insert location.
* <p>
* //TODO ATTENTION: This tests deletes all stored tracks in the database.
* So, if it is executed on a real device, data might be lost.
*/
@RunWith(AndroidJUnit4.class)
public class TrackRecordingServiceTestLocation {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
private TrackRecordingServiceInterface service;
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@Before
public void setUp() throws TimeoutException {
// Set up the mock content resolver
ContentProvider customContentProvider = new CustomContentProvider() {
};
customContentProvider.attachInfo(context, null);
contentProviderUtils = new ContentProviderUtils(context);
// Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
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();
}
// Ensure that the database is empty after every test
contentProviderUtils.deleteAllTracks(context);
}
@MediumTest
@Test
public void testOnLocationChangedAsync_movingAccurate() throws Exception {
// given
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);
service.endCurrentTrack();
// then
Assert.assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
Assert.assertEquals(6, trackPoints.size());
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 {
// given
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);
service.endCurrentTrack();
// then
Assert.assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
Assert.assertEquals(2, trackPoints.size());
Assert.assertEquals(45.000005, trackPoints.get(1).getLatitude(), 0.01);
}
// @MediumTest
// @Test
// public void testOnLocationChangedAsync_repeatedTime() throws Exception {
// // when
// TrackRecordingServiceTest.insertLocation(service, 45.0, 35.0, 5, 15, 5);
// TrackRecordingServiceTest.insertLocation(service, 55.0, 35.0, 5, 15, 5);
// TrackRecordingServiceTest.insertLocation(service, 65.0, 35.0, 5, 15, 5);
//
// 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_idle() throws Exception {
// given
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);
service.endCurrentTrack();
// then
Assert.assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
Assert.assertEquals(3, trackPoints.size());
Assert.assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01);
Assert.assertEquals(2, trackPoints.get(1).getAccuracy(), 0.01);
Assert.assertEquals(6, trackPoints.get(2).getAccuracy(), 0.01);
}
@MediumTest
@Test
public void testOnLocationChangedAsync_idle_withMovement() throws Exception {
// given
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);
service.endCurrentTrack();
// then
Assert.assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
Assert.assertEquals(4, trackPoints.size());
Assert.assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01);
Assert.assertEquals(2, trackPoints.get(1).getAccuracy(), 0.01);
Assert.assertEquals(5, trackPoints.get(2).getAccuracy(), 0.01); //TODO Check why this trackPoint is inserted.
Assert.assertEquals(6, trackPoints.get(3).getAccuracy(), 0.01);
}
@MediumTest
@Test
public void testOnLocationChangedAsync_idle_withSensorData() throws Exception {
// given
long trackId = service.startNewTrack();
service.setRemoteSensorManager(new BluetoothRemoteSensorManager(context) {
@Override
public boolean isEnabled() {
return true;
}
@Override
public SensorDataSet getSensorData() {
SensorDataSet sensorDataSet = new SensorDataSet();
sensorDataSet.set(new SensorDataHeartRate("sensorName", "sensorAddress", 5f));
return sensorDataSet;
}
});
// 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);
service.endCurrentTrack();
// then
Assert.assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
Assert.assertEquals(6, trackPoints.size());
Assert.assertEquals(0, trackPoints.get(0).getAccuracy(), 0.01);
Assert.assertEquals(1, trackPoints.get(1).getAccuracy(), 0.01);
Assert.assertEquals(2, trackPoints.get(2).getAccuracy(), 0.01);
Assert.assertEquals(3, trackPoints.get(3).getAccuracy(), 0.01);
Assert.assertEquals(4, trackPoints.get(4).getAccuracy(), 0.01);
Assert.assertEquals(5, trackPoints.get(5).getAccuracy(), 0.01);
}
@MediumTest
@Test
public void testOnLocationChangedAsync_segment() throws Exception {
// given
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);
service.endCurrentTrack();
// then
Assert.assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
Assert.assertEquals(7, trackPoints.size());
Assert.assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01);
Assert.assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
Assert.assertEquals(2, trackPoints.get(2).getAccuracy(), 0.01);
Assert.assertEquals(3, trackPoints.get(3).getAccuracy(), 0.01);
Assert.assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(4).getLatitude(), 0.01);
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,7 +26,7 @@ public class AnnouncementUtilsTest {
stats.setTotalElevationGain(6000);
// when
String announcement = AnnouncementUtils.getAnnouncement(context, stats);
String announcement = AnnouncementUtils.getAnnouncement(context, stats, "airplane");
// then
Assert.assertEquals("OpenTracks total distance 20.00 kilometers in 5 minutes 0 seconds at 240.0 kilometers per hour", announcement);
@@ -0,0 +1,76 @@
package de.dennisguse.opentracks.util;
import android.bluetooth.BluetoothGattCharacteristic;
import org.junit.Assert;
import org.junit.Test;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
public class BluetoothUtilsTest {
@Test
public void parseHeartRate_uint8() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEART_RATE_SERVICE_UUID, 0, 0);
characteristic.setValue(new byte[]{0x02, 0x3C});
// when
int heartRate = BluetoothUtils.parseHeartRate(characteristic);
// then
Assert.assertEquals(60, heartRate);
}
@Test
public void parseHeartRate_uint16() {
// given
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.HEART_RATE_SERVICE_UUID, 0, 0);
characteristic.setValue(new byte[]{0x01, 0x01, 0x01});
// when
int heartRate = BluetoothUtils.parseHeartRate(characteristic);
// then
Assert.assertEquals(257, heartRate);
}
@Test
public void parseCyclingSpeedCadence_crankOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
characteristic.setValue(new byte[]{0x01, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99});
// when
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
Assert.assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
Assert.assertNull(sensor.getSpeed());
}
@Test
public void parseCyclingSpeedCadence_wheelOnly() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
characteristic.setValue(new byte[]{0x02, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
// when
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
Assert.assertNull(sensor.getCadence());
Assert.assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
}
@Test
public void parseCyclingSpeedCadence_crankWheel() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, 0, 0);
characteristic.setValue(new byte[]{0x03, (byte) 0xC8, 0x00, 0x00, 0x00, 0x06, (byte) 0x99, (byte) 0xE1, 0x00, 0x45, (byte) 0x99});
// when
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
Assert.assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
Assert.assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
}
}
@@ -0,0 +1,52 @@
package de.dennisguse.opentracks.util;
import org.junit.Assert;
import org.junit.Test;
import de.dennisguse.opentracks.content.data.TrackPoint;
public class TrackPointUtilsTest {
@Test
public void fixTime_none() {
// given
long time = System.currentTimeMillis();
TrackPoint trackPoint = new TrackPoint();
trackPoint.setTime(time);
// when
TrackPointUtils.fixTime(trackPoint);
// then
Assert.assertEquals(time, trackPoint.getTime());
}
@Test
public void fixTime_0() {
// given
long time = System.currentTimeMillis();
TrackPoint trackPoint = new TrackPoint();
trackPoint.setTime(0);
// when
TrackPointUtils.fixTime(trackPoint);
// then
Assert.assertEquals(time, trackPoint.getTime(), 1000);
}
@Test
public void fixTime_gpsWeekRollover() {
// given
long time = System.currentTimeMillis();
TrackPoint trackPoint = new TrackPoint();
trackPoint.setTime(time - 1024 * UnitConversions.ONE_WEEK_MS);
// when
TrackPointUtils.fixTime(trackPoint);
// then
Assert.assertEquals(time, trackPoint.getTime());
}
}
+1 -3
View File
@@ -15,9 +15,7 @@ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.dennisguse.opentracks"
android:installLocation="auto"
android:versionCode="3330"
android:versionName="v3.6.0"> <!-- For FDroid. -->
android:installLocation="auto">
<!-- Permission to initialize services -->
<application
android:name=".Startup"
@@ -315,6 +315,6 @@ public class MarkerEditActivity extends AbstractActivity {
waypoint.setDescription(waypointDescription.getText().toString());
waypoint.setPhotoUrl(photoUri != null ? photoUri.toString() : null);
new ContentProviderUtils(this).updateWaypoint(waypoint);
new ContentProviderUtils(this).updateWaypoint(this, waypoint);
}
}
@@ -62,6 +62,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
public static final String EXTRA_RECORDING_REQUEST = "request_recording";
private static final String TAG = TrackDetailActivity.class.getSimpleName();
@@ -78,6 +79,8 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
// From intent
private long trackId;
private boolean isRecordingRequest = false;
// Preferences
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused;
@@ -99,6 +102,9 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
Log.d(TAG, "could not get TrackRecordingService");
return;
}
if (!isRecordingRequest) {
return;
}
// Starts or resumes a track.
int msg;
@@ -119,6 +125,8 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
trackDataHub.loadTrack(trackId);
trackController.update(true, false);
trackController.onResume(true, recordingTrackPaused);
isRecordingRequest = false;
}
}
};
@@ -384,6 +392,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
deleteTracks(new long[]{trackId});
return true;
case R.id.track_detail_resume_track:
isRecordingRequest = true;
startRecording();
return true;
case R.id.track_detail_settings:
@@ -419,6 +428,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
private void handleIntent(Intent intent) {
trackId = intent.getLongExtra(EXTRA_TRACK_ID, -1L);
isRecordingRequest = intent.getBooleanExtra(EXTRA_RECORDING_REQUEST, false);
long markerId = intent.getLongExtra(EXTRA_MARKER_ID, -1L);
if (markerId != -1L) {
// Use the trackId from the marker
@@ -28,7 +28,6 @@ import android.location.LocationManager;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
@@ -206,7 +205,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
if (!PreferencesUtils.isRecording(recordingTrackId)) {
// Not recording -> Recording
updateMenuItems(false, true);
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class);
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_RECORDING_REQUEST, true);
startActivity(newIntent);
} else if (recordingTrackPaused) {
// Paused -> Resume
@@ -352,7 +352,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
searchMenuItem = menu.findItem(R.id.track_list_search);
ActivityUtils.configureSearchWidget(this, searchMenuItem, trackController);
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
return super.onCreateOptionsMenu(menu);
@@ -382,11 +381,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
} else {
startGps = !ServiceUtils.isTrackRecordingServiceRunning(this);
// Show toast
Toast toast = Toast.makeText(this, startGps ? R.string.gps_starting : R.string.gps_stopping, Toast.LENGTH_SHORT);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
// Invoke trackRecordingService
if (startGps) {
trackRecordingServiceConnection.startAndBind(this);
@@ -3,7 +3,7 @@ package de.dennisguse.opentracks.chart;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -25,7 +25,7 @@ public class ChartPoint {
this.elevation = elevation;
}
public ChartPoint(@NonNull TrackStatisticsUpdater trackStatisticsUpdater, SensorDataSet sensorDataSet, boolean chartByDistance, boolean metricUnits) {
public ChartPoint(@NonNull TrackStatisticsUpdater trackStatisticsUpdater, TrackPoint trackPoint, boolean chartByDistance, boolean metricUnits) {
TrackStatistics trackStatistics = trackStatisticsUpdater.getTrackStatistics();
if (chartByDistance) {
@@ -48,15 +48,15 @@ public class ChartPoint {
speed *= UnitConversions.KM_TO_MI;
}
pace = speed == 0 ? 0.0 : 60.0 / speed;
if (sensorDataSet != null) {
if (sensorDataSet.hasHeartRate()) {
heartRate = sensorDataSet.getHeartRate();
if (trackPoint != null) {
if (trackPoint.hasHeartRate()) {
heartRate = trackPoint.getHeartRate_bpm();
}
if (sensorDataSet.hasCadence()) {
cadence = sensorDataSet.getCadence();
if (trackPoint.hasCyclingCadence()) {
cadence = trackPoint.getCyclingCadence_rpm();
}
if (sensorDataSet.hasPower()) {
power = sensorDataSet.getPower();
if (trackPoint.hasPower()) {
power = trackPoint.getPower();
}
}
}
@@ -846,7 +846,7 @@ public class ChartView extends View {
for (int i = 0; i < seriesList.size(); i++) {
ChartValueSeries chartValueSeries = seriesList.get(i);
if (chartValueSeries.isChartPointValid(dataPoint)) {
if (!chartValueSeries.isChartPointValid(dataPoint)) {
continue;
}
@@ -126,10 +126,14 @@ public class DescriptionGenerator {
writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute, lineBreak);
// Max elevation
writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak);
if (stats.hasElevationMax()) {
writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak);
}
// Min elevation
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
if (stats.hasElevationMin()) {
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
}
// Elevation gain
writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
@@ -16,11 +16,6 @@
package de.dennisguse.opentracks.content.data;
import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
@@ -40,9 +35,6 @@ public class Track {
private TrackStatistics trackStatistics = new TrackStatistics();
// Location points (which may not have been loaded)
private List<TrackPoint> trackPoints = new ArrayList<>();
public Track() {
}
@@ -93,17 +85,4 @@ public class Track {
public void setTrackStatistics(TrackStatistics trackStatistics) {
this.trackStatistics = trackStatistics;
}
@VisibleForTesting
public void addTrackPoint(TrackPoint location) {
trackPoints.add(location);
}
public List<TrackPoint> getTrackPoints() {
return trackPoints;
}
public void setTrackPoints(ArrayList<TrackPoint> trackPoints) {
this.trackPoints = trackPoints;
}
}
@@ -20,8 +20,6 @@ import android.location.LocationManager;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
/**
* This class extends the standard Android location with extra information.
*
@@ -31,6 +29,10 @@ public class TrackPoint {
private final Location location;
private Float heartRate_bpm = null;
private Float cyclingCadence_rpm = null;
private Float power = null;
public TrackPoint() {
this.location = new Location("");
}
@@ -39,17 +41,13 @@ public class TrackPoint {
this.location = location;
}
public TrackPoint(@NonNull Location location, SensorDataSet sensorDataSet) {
this.location = location;
this.sensorDataSet = sensorDataSet;
}
public TrackPoint(@NonNull TrackPoint trackPoint, SensorDataSet sensorDataSet) {
public TrackPoint(@NonNull TrackPoint trackPoint) {
this.location = trackPoint.getLocation();
this.sensorDataSet = sensorDataSet;
}
private SensorDataSet sensorDataSet = null;
this.heartRate_bpm = trackPoint.getHeartRate_bpm();
this.cyclingCadence_rpm = trackPoint.getCyclingCadence_rpm();
this.power = trackPoint.getPower();
}
public TrackPoint(double latitude, double longitude, Double altitude, long time) {
location = new Location(LocationManager.GPS_PROVIDER);
@@ -90,14 +88,6 @@ public class TrackPoint {
return location;
}
public SensorDataSet getSensorDataSet() {
return sensorDataSet;
}
public void setSensorDataSet(SensorDataSet sensorDataSet) {
this.sensorDataSet = sensorDataSet;
}
public double getLatitude() {
return location.getLatitude();
}
@@ -181,4 +171,51 @@ public class TrackPoint {
public float bearingTo(@NonNull Location dest) {
return location.bearingTo(dest);
}
// Sensor data
public boolean hasSensorData() {
return hasHeartRate() || hasCyclingCadence() || hasPower();
}
public boolean hasHeartRate() {
return heartRate_bpm != null && heartRate_bpm > 0;
}
public float getHeartRate_bpm() {
return heartRate_bpm;
}
public void setHeartRate_bpm(Float heartRate_bpm) {
this.heartRate_bpm = heartRate_bpm;
}
public boolean hasCyclingCadence() {
return cyclingCadence_rpm != null;
}
public Float getCyclingCadence_rpm() {
return cyclingCadence_rpm;
}
public void setCyclingCadence_rpm(Float cyclingCadence_rpm) {
this.cyclingCadence_rpm = cyclingCadence_rpm;
}
public boolean hasPower() {
return power != null;
}
public Float getPower() {
return power;
}
public void setPower(Float power) {
this.power = power;
}
@NonNull
@Override
public String toString() {
return "time=" + getTime() + ": lat=" + getLatitude() + " lng=" + getLongitude() + " acc=" + getAccuracy();
}
}
@@ -38,14 +38,15 @@ public final class Waypoint {
private long trackId = -1L;
private double length = 0.0;
private long duration = 0;
private Location location = null;
private Location location;
private String photoUrl = "";
@VisibleForTesting
public Waypoint() {
public Waypoint(@NonNull TrackPoint trackPoint) {
this.location = trackPoint.getLocation();
}
public Waypoint(Location location) {
public Waypoint(@NonNull Location location) {
this.location = location;
}
@@ -40,7 +40,6 @@ import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.FileUtils;
@@ -55,19 +54,13 @@ public class ContentProviderUtils {
private static final String TAG = ContentProviderUtils.class.getSimpleName();
private static final int MAX_LATITUDE = 90000000;
/**
* The authority (the first part of the URI) for the app's content provider.
*/
// The authority (the first part of the URI) for the app's content provider.
static final String AUTHORITY_PACKAGE = BuildConfig.APPLICATION_ID + ".content";
/**
* The base URI for the app's content provider.
*/
// The base URI for the app's content provider.
public static final String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
/**
* Maximum number of waypoints that will be loaded at one time.
*/
// Maximum number of waypoints that will be loaded at one time.
public static final int MAX_LOADED_WAYPOINTS_POINTS = 10000;
private static final String ID_SEPARATOR = ",";
@@ -87,7 +80,7 @@ public class ContentProviderUtils {
}
/**
* Clears a track: removes waypoints and trackpoints.
* Clears a track: removes waypoints and trackPoints.
* Only keeps the track id.
*
* @param trackId the track id
@@ -168,7 +161,7 @@ public class ContentProviderUtils {
}
/**
* Deletes all tracks (including waypoints and track points).
* Deletes all tracks (including waypoints and trackPoints).
*/
public void deleteAllTracks(Context context) {
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, null, null);
@@ -177,7 +170,7 @@ public class ContentProviderUtils {
contentResolver.delete(TracksColumns.CONTENT_URI, null, null);
File dir = FileUtils.getPhotoDir(context);
deleteDirectoryRecurse(dir);
FileUtils.deleteDirectoryRecurse(dir);
}
/**
@@ -188,13 +181,15 @@ public class ContentProviderUtils {
public void deleteTrack(Context context, long trackId) {
deleteTrackPointsAndWaypoints(context, trackId);
// Delete track last since it triggers a database vaccum call
contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?",
new String[]{Long.toString(trackId)});
// Delete track folder resources.
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
// Delete track last since it triggers a database vacuum call
contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?", new String[]{Long.toString(trackId)});
}
/**
* Deletes track points and waypoints of a track.
* Deletes trackPoints and waypoints of a track.
*
* @param trackId the track id
*/
@@ -203,29 +198,13 @@ public class ContentProviderUtils {
String[] selectionArgs = new String[]{Long.toString(trackId)};
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, where, selectionArgs);
contentResolver.delete(WaypointsColumns.CONTENT_URI, WaypointsColumns.TRACKID + "=?",
new String[]{Long.toString(trackId)});
deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
}
/**
* Delete the directory recursively.
*
* @param dir the directory
*/
private void deleteDirectoryRecurse(File dir) {
if (dir != null && dir.exists() && dir.isDirectory()) {
for (File child : dir.listFiles()) {
deleteDirectoryRecurse(child);
}
dir.delete();
}
contentResolver.delete(WaypointsColumns.CONTENT_URI, WaypointsColumns.TRACKID + "=?", new String[]{Long.toString(trackId)});
}
/**
* Gets all the tracks.
* If no track exists, an empty list is returned.
* NOTE: the returned tracks do not have any track points attached.
*
* @return the tracks do not have any trackPoints attached.
*/
@VisibleForTesting
public List<Track> getAllTracks() {
@@ -241,9 +220,6 @@ public class ContentProviderUtils {
return tracks;
}
/**
* Gets the last track or null.
*/
public Track getLastTrack() {
try (Cursor cursor = getTrackCursor(null, null, TracksColumns.STARTTIME + " DESC")) {
// Using the same order as shown in the track list
@@ -255,10 +231,8 @@ public class ContentProviderUtils {
}
/**
* Gets a track by a track id or null
* Note that the returned track doesn't have any track points attached.
*
* @param trackId the track id.
* @return the track doesn't have any trackPoints attached
*/
public Track getTrack(long trackId) {
if (trackId < 0) {
@@ -286,7 +260,7 @@ public class ContentProviderUtils {
/**
* Inserts a track.
* NOTE: This doesn't insert any track points.
* NOTE: This doesn't insert any trackPoints.
*
* @param track the track
* @return the content provider URI of the inserted track.
@@ -297,13 +271,12 @@ public class ContentProviderUtils {
/**
* Updates a track.
* NOTE: This doesn't update any track points.
* NOTE: This doesn't update any trackPoints.
*
* @param track the track
*/
public void updateTrack(Track track) {
contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(track),
TracksColumns._ID + "=?", new String[]{Long.toString(track.getId())});
contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(track), TracksColumns._ID + "=?", new String[]{Long.toString(track.getId())});
}
private ContentValues createContentValues(Track track) {
@@ -407,35 +380,17 @@ public class ContentProviderUtils {
return waypoint;
}
/**
* Deletes a waypoint.
* If deleting a statistics waypoint, this will also correct the next statistics waypoint after the deleted one to reflect the deletion.
* The generator is used to update the next statistics waypoint.
*
* @param waypointId the waypoint id
*/
public void deleteWaypoint(long waypointId) {
public void deleteWaypoint(Context context, long waypointId) {
final Waypoint waypoint = getWaypoint(waypointId);
if (waypoint != null && waypoint.hasPhoto()) {
Uri uri = waypoint.getPhotoURI();
File file = new File(uri.getPath());
if (file.exists()) {
File parent = file.getParentFile();
file.delete();
if (parent.listFiles().length == 0) {
parent.delete();
}
}
}
deleteWaypointPhoto(context, waypoint);
contentResolver.delete(WaypointsColumns.CONTENT_URI, WaypointsColumns._ID + "=?", new String[]{Long.toString(waypointId)});
}
/**
* Gets the next waypoint number for a type.
* Returns -1 if not able to get the next waypoint number.
* Gets the next waypoint number.
*
* @param trackId the track id
* @return -1 if not able to get the next waypoint number.
*/
public int getNextWaypointNumber(long trackId) {
if (trackId < 0) {
@@ -452,12 +407,6 @@ public class ContentProviderUtils {
return -1;
}
/**
* Gets a waypoint from a waypoint id.
* Returns null if not found.
*
* @param waypointId the waypoint id
*/
public Waypoint getWaypoint(long waypointId) {
if (waypointId < 0) {
return null;
@@ -478,8 +427,7 @@ public class ContentProviderUtils {
* @param selection the selection. Can be null
* @param selectionArgs the selection arguments. Can be null
* @param sortOrder the sort order. Can be null
* @param maxWaypoints the maximum number of waypoints to return. -1 for no
* limit
* @param maxWaypoints the maximum number of waypoints to return. -1 for no limit
*/
public Cursor getWaypointCursor(String selection, String[] selectionArgs, String sortOrder, int maxWaypoints) {
return getWaypointCursor(null, selection, selectionArgs, sortOrder, maxWaypoints);
@@ -536,12 +484,13 @@ public class ContentProviderUtils {
String[] projection = new String[]{"count(*) AS count"};
String selection = WaypointsColumns.TRACKID + "=?";
String[] selectionArgs = new String[]{Long.toString(trackId)};
Cursor cursor = contentResolver.query(WaypointsColumns.CONTENT_URI, projection, selection, selectionArgs, WaypointsColumns._ID);
cursor.moveToFirst();
int count = cursor.getInt(0);
cursor.close();
return count;
try (Cursor cursor = contentResolver.query(WaypointsColumns.CONTENT_URI, projection, selection, selectionArgs, WaypointsColumns._ID)) {
if (cursor == null) {
return 0;
}
cursor.moveToFirst();
return cursor.getInt(0);
}
}
/**
@@ -555,14 +504,38 @@ public class ContentProviderUtils {
return contentResolver.insert(WaypointsColumns.CONTENT_URI, createContentValues(waypoint));
}
/**
* Delete waypoint's photo if any.
*
* @param context the context object.
* @param waypoint the waypoint object.
*/
private void deleteWaypointPhoto(Context context, Waypoint waypoint) {
if (waypoint != null && waypoint.hasPhoto()) {
Uri uri = waypoint.getPhotoURI();
File file = FileUtils.getPhotoFileIfExists(context, waypoint.getTrackId(), uri);
if (file.exists()) {
File parent = file.getParentFile();
file.delete();
if (parent.listFiles().length == 0) {
parent.delete();
}
}
}
}
/**
* Updates a waypoint.
* Returns true if successful.
*
* @param waypoint the waypoint
* @param updatedWaypoint the waypoint with updated data.
*/
public boolean updateWaypoint(Waypoint waypoint) {
int rows = contentResolver.update(WaypointsColumns.CONTENT_URI, createContentValues(waypoint), WaypointsColumns._ID + "=?", new String[]{Long.toString(waypoint.getId())});
public boolean updateWaypoint(Context context, Waypoint updatedWaypoint) {
Waypoint savedWaypoint = getWaypoint(updatedWaypoint.getId());
if (!updatedWaypoint.hasPhoto()) {
deleteWaypointPhoto(context, savedWaypoint);
}
int rows = contentResolver.update(WaypointsColumns.CONTENT_URI, createContentValues(updatedWaypoint), WaypointsColumns._ID + "=?", new String[]{Long.toString(updatedWaypoint.getId())});
return rows == 1;
}
@@ -582,19 +555,17 @@ public class ContentProviderUtils {
values.put(WaypointsColumns.DURATION, waypoint.getDuration());
Location location = waypoint.getLocation();
if (location != null) {
values.put(WaypointsColumns.LONGITUDE, (int) (location.getLongitude() * 1E6));
values.put(WaypointsColumns.LATITUDE, (int) (location.getLatitude() * 1E6));
values.put(WaypointsColumns.TIME, location.getTime());
if (location.hasAltitude()) {
values.put(WaypointsColumns.ALTITUDE, location.getAltitude());
}
if (location.hasAccuracy()) {
values.put(WaypointsColumns.ACCURACY, location.getAccuracy());
}
if (location.hasBearing()) {
values.put(WaypointsColumns.BEARING, location.getBearing());
}
values.put(WaypointsColumns.LONGITUDE, (int) (location.getLongitude() * 1E6));
values.put(WaypointsColumns.LATITUDE, (int) (location.getLatitude() * 1E6));
values.put(WaypointsColumns.TIME, location.getTime());
if (location.hasAltitude()) {
values.put(WaypointsColumns.ALTITUDE, location.getAltitude());
}
if (location.hasAccuracy()) {
values.put(WaypointsColumns.ACCURACY, location.getAccuracy());
}
if (location.hasBearing()) {
values.put(WaypointsColumns.BEARING, location.getBearing());
}
values.put(WaypointsColumns.PHOTOURL, waypoint.getPhotoUrl());
@@ -623,8 +594,8 @@ public class ContentProviderUtils {
/**
* Fills a {@link TrackPoint} from a cursor.
*
* @param cursor the cursor pointing to a trackPoint.
* @param indexes the cached track points indexes
* @param cursor the cursor pointing to a trackPoint.
* @param indexes the cached trackPoints indexes
*/
static TrackPoint fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes) {
TrackPoint trackPoint = new TrackPoint();
@@ -651,30 +622,30 @@ public class ContentProviderUtils {
trackPoint.setBearing(cursor.getFloat(indexes.bearingIndex));
}
float heartRate = cursor.isNull(indexes.sensorHeartRateIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorHeartRateIndex);
float cadence = cursor.isNull(indexes.sensorCadenceIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorCadenceIndex);
float power = cursor.isNull(indexes.sensorPowerIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorPowerIndex);
trackPoint.setSensorDataSet(new SensorDataSet(heartRate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, trackPoint.getTime()));
if (!cursor.isNull(indexes.sensorHeartRateIndex)) {
trackPoint.setHeartRate_bpm(cursor.getFloat(indexes.sensorHeartRateIndex));
}
if (!cursor.isNull(indexes.sensorCadenceIndex)) {
trackPoint.setCyclingCadence_rpm(cursor.getFloat(indexes.sensorCadenceIndex));
}
if (!cursor.isNull(indexes.sensorPowerIndex)) {
trackPoint.setPower(cursor.getFloat(indexes.sensorPowerIndex));
}
return trackPoint;
}
/**
* Inserts multiple trackPoints points.
* Inserts multiple trackPoints.
*
* @param trackPoints an array of trackPoints
* @param length the number of trackPoints (from the beginning of the array) to
* insert, or -1 for all of them
* @param trackId the trackPoints id
* @return the number of points inserted
* @param trackId the trackPoints id
* @return the number of trackPoints inserted
*/
public int bulkInsertTrackPoint(TrackPoint[] trackPoints, int length, long trackId) {
if (length == -1) {
length = trackPoints.length;
}
ContentValues[] values = new ContentValues[length];
for (int i = 0; i < length; i++) {
//TODO Only used for testing and file import; might be better to replace it.
public int bulkInsertTrackPoint(TrackPoint[] trackPoints, long trackId) {
ContentValues[] values = new ContentValues[trackPoints.length];
for (int i = 0; i < values.length; i++) {
values[i] = createContentValues(trackPoints[i], trackId);
}
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
@@ -691,9 +662,7 @@ public class ContentProviderUtils {
if (trackId < 0) {
return -1L;
}
String selection = TrackPointsColumns._ID + "=(select min(" + TrackPointsColumns._ID
+ ") from " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID
+ "=?)";
String selection = TrackPointsColumns._ID + "=(SELECT MIN(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=?)";
String[] selectionArgs = new String[]{Long.toString(trackId)};
try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) {
@@ -714,9 +683,7 @@ public class ContentProviderUtils {
if (trackId < 0) {
return -1L;
}
String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID
+ ") from " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID
+ "=?)";
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") from " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=?)";
String[] selectionArgs = new String[]{Long.toString(trackId)};
try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) {
@@ -727,19 +694,17 @@ public class ContentProviderUtils {
}
/**
* Gets the track point id of a location.
* Gets the trackPoint id for a location.
*
* @param trackId the track id
* @param location the location
* @return track point id if the location is in the track. -1L otherwise.
* @return trackPoint id if the location is in the track. -1L otherwise.
*/
public long getTrackPointId(long trackId, Location location) {
if (trackId < 0) {
return -1L;
}
String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID
+ ") from " + TrackPointsColumns.TABLE_NAME
+ " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TIME + "=?)";
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TIME + "=?)";
String[] selectionArgs = new String[]{Long.toString(trackId), Long.toString(location.getTime())};
try (Cursor cursor = getTrackPointCursor(new String[]{TrackPointsColumns._ID}, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) {
@@ -762,7 +727,7 @@ public class ContentProviderUtils {
* Creates a location cursor. The caller owns the returned cursor and is responsible for closing it.
*
* @param trackId the track id
* @param startTrackPointId the starting track point id. -1L to ignore
* @param startTrackPointId the starting trackPoint id. -1L to ignore
* @param maxLocations maximum number of locations to return. -1 for no limit
* @param descending true to sort the result in descending order (latest location first)
*/
@@ -775,8 +740,7 @@ public class ContentProviderUtils {
String[] selectionArgs;
if (startTrackPointId >= 0) {
String comparison = descending ? "<=" : ">=";
selection = TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns._ID + comparison
+ "?";
selection = TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns._ID + comparison + "?";
selectionArgs = new String[]{Long.toString(trackId), Long.toString(startTrackPointId)};
} else {
selection = TrackPointsColumns.TRACKID + "=?";
@@ -804,19 +768,17 @@ public class ContentProviderUtils {
if (trackId < 0) {
return null;
}
String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID + ") from "
+ TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND "
+ TrackPointsColumns.LATITUDE + "<=" + MAX_LATITUDE + ")";
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.LATITUDE + "<=" + MAX_LATITUDE + ")";
String[] selectionArgs = new String[]{Long.toString(trackId)};
return findTrackPointBy(selection, selectionArgs);
}
/**
* Inserts a track point.
* Inserts a trackPoint.
*
* @param trackPoint the trackPoint
* @param trackId the track id
* @return the content provider URI of the inserted track point
* @param trackId the track id
* @return the content provider URI of the inserted trackPoint
*/
public Uri insertTrackPoint(TrackPoint trackPoint, long trackId) {
return contentResolver.insert(TrackPointsColumns.CONTENT_URI_BY_ID, createContentValues(trackPoint, trackId));
@@ -826,7 +788,7 @@ public class ContentProviderUtils {
* Creates the {@link ContentValues} for a {@link TrackPoint}.
*
* @param trackPoint the trackPoint
* @param trackId the track id
* @param trackId the track id
*/
private ContentValues createContentValues(TrackPoint trackPoint, long trackId) {
ContentValues values = new ContentValues();
@@ -848,16 +810,14 @@ public class ContentProviderUtils {
values.put(TrackPointsColumns.BEARING, trackPoint.getBearing());
}
//SensorData
SensorDataSet sensorDataSet = trackPoint.getSensorDataSet();
if (sensorDataSet != null && sensorDataSet.hasHeartRate()) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.getSensorDataSet().getHeartRate());
if (trackPoint.hasHeartRate()) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.getHeartRate_bpm());
}
if (sensorDataSet != null && sensorDataSet.hasCadence()) {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getSensorDataSet().getCadence());
if (trackPoint.hasCyclingCadence()) {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getCyclingCadence_rpm());
}
if (sensorDataSet != null && sensorDataSet.hasPower()) {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getSensorDataSet().getPower());
if (trackPoint.hasPower()) {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getPower());
}
return values;
}
@@ -870,7 +830,7 @@ public class ContentProviderUtils {
* When done with iteration, {@link TrackPointIterator#close()} must be called.
*
* @param trackId the track id
* @param startTrackPointId the starting track point id. -1L to ignore
* @param startTrackPointId the starting trackPoint id. -1L to ignore
* @param descending true to sort the result in descending order (latest location first)
*/
public TrackPointIterator getTrackPointLocationIterator(final long trackId, final long startTrackPointId, final boolean descending) {
@@ -887,7 +847,7 @@ public class ContentProviderUtils {
}
/**
* Gets a track point cursor.
* Gets a trackPoint cursor.
*
* @param projection the projection
* @param selection the selection
@@ -898,6 +858,24 @@ public class ContentProviderUtils {
return contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, projection, selection, selectionArgs, sortOrder);
}
@VisibleForTesting
public List<TrackPoint> getTrackPoints(long trackId) {
List<TrackPoint> trackPoints = null;
try (Cursor trackPointCursor = getTrackPointCursor(trackId, -1L, -1, false)) {
if (trackPointCursor != null) {
trackPointCursor.moveToFirst();
trackPoints = new ArrayList<>(trackPointCursor.getCount());
for (int i = 0; i < trackPointCursor.getCount(); i++) {
trackPoints.add(createTrackPoint(trackPointCursor));
trackPointCursor.moveToNext();
}
}
}
return trackPoints;
}
int getDefaultCursorBatchSize() {
return defaultCursorBatchSize;
}
@@ -915,7 +893,7 @@ public class ContentProviderUtils {
/**
* Formats an array of IDs as comma separated string value
*
* @param ids array with IDs
* @param ids array with IDs
* @return comma separated list of ids
*/
public static String formatIdListForUri(long[] ids) {
@@ -0,0 +1,39 @@
package de.dennisguse.opentracks.content.sensor;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
public class SensorData {
private String sensorAddress;
private String sensorName;
private long timestamp_ms;
SensorData(String sensorAddress, String sensorName) {
this(sensorAddress, sensorName, System.currentTimeMillis());
}
@VisibleForTesting
SensorData(String sensorAddress, String sensorName, long timestamp_ms) {
this.sensorAddress = sensorAddress;
this.sensorName = sensorName;
this.timestamp_ms = timestamp_ms;
}
public String getSensorAddress() {
return sensorAddress;
}
public String getSensorName() {
return sensorName;
}
/**
* Is the data recent considering the current time.
*/
public boolean isRecent() {
return timestamp_ms + BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS > System.currentTimeMillis();
}
}
@@ -0,0 +1,175 @@
package de.dennisguse.opentracks.content.sensor;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.util.UintUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* Provides cadence in rpm and speed in milliseconds from Bluetooth LE Cycling Cadence and Speed sensors.
* <p>
* https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=261449
*/
public final class SensorDataCycling {
private static final String TAG = SensorDataCycling.class.getSimpleName();
private static final int INVALID_VALUE_INT = -1;
private static final float INVALID_VALUE_FLOAT = Float.NaN;
private SensorDataCycling() {
}
public static class Cadence extends SensorData {
private long crankRevolutionsCount; // UINT32
private int crankRevolutionsTime; // UINT16; 1/1024s
private float cadence_rpm = INVALID_VALUE_FLOAT;
public Cadence(String sensorAddress, String sensorName, long crankRevolutionsCount, int crankRevolutionsTime) {
super(sensorAddress, sensorName);
this.crankRevolutionsCount = crankRevolutionsCount;
this.crankRevolutionsTime = crankRevolutionsTime;
}
/**
* Workaround for Wahoo CADENCE: provides speed instead of cadence
*/
public Cadence(@NonNull SensorDataCycling.Speed speed) {
this(speed.getSensorAddress(), speed.getSensorName(), speed.getWheelRevolutionsCount(), speed.getWheelRevolutionsTime());
}
public boolean hasData() {
return crankRevolutionsCount != INVALID_VALUE_INT && crankRevolutionsTime != INVALID_VALUE_INT;
}
public long getCrankRevolutionsCount() {
return crankRevolutionsCount;
}
public int getCrankRevolutionsTime() {
return crankRevolutionsTime;
}
public boolean hasCadence_rpm() {
return !Float.isNaN(cadence_rpm);
}
public float getCadence_rpm() {
return cadence_rpm;
}
public void compute(Cadence previous) {
if (hasData() && previous != null && previous.hasData()) {
long timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024 * UnitConversions.S_TO_MS;
if (timeDiff_ms <= 0) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
cadence_rpm = INVALID_VALUE_FLOAT;
} else {
long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX);
float cadence_ms = crankDiff / (float) timeDiff_ms;
cadence_rpm = (float) (cadence_ms / UnitConversions.MS_TO_S / UnitConversions.S_TO_MIN);
}
}
}
@NonNull
@Override
public String toString() {
return "cadence=" + getCadence_rpm() + "_" + getCrankRevolutionsTime();
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof Cadence)) return false;
Cadence comp = (Cadence) obj;
return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime();
}
}
public static class Speed extends SensorData {
private int wheelRevolutionsCount; // UINT16
private int wheelRevolutionsTime; // UINT16; 1/1024s
private float speed_mps = INVALID_VALUE_FLOAT;
public Speed(String sensorAddress, String sensorName, int wheelRevolutionsCount, int wheelRevolutionsTime) {
super(sensorAddress, sensorName);
this.wheelRevolutionsCount = wheelRevolutionsCount;
this.wheelRevolutionsTime = wheelRevolutionsTime;
}
public boolean hasData() {
return wheelRevolutionsCount != INVALID_VALUE_INT && wheelRevolutionsTime != INVALID_VALUE_INT;
}
public int getWheelRevolutionsCount() {
return wheelRevolutionsCount;
}
public int getWheelRevolutionsTime() {
return wheelRevolutionsTime;
}
public boolean hasSpeed_mps() {
return !Float.isNaN(speed_mps);
}
public float getSpeed_mps() {
return speed_mps;
}
public void compute(Speed previous, int wheel_circumference_mm) {
if (hasData() && previous != null && previous.hasData()) {
long timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024 * UnitConversions.S_TO_MS;
if (timeDiff_ms <= 0) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
speed_mps = INVALID_VALUE_FLOAT;
} else {
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
double timeDiff_s = timeDiff_ms * UnitConversions.MS_TO_S;
speed_mps = (float) (wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M / timeDiff_s);
}
}
}
@NonNull
@Override
public String toString() {
return "speed=" + getSpeed_mps() + "_" + getWheelRevolutionsTime();
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof Speed)) return false;
Speed comp = (Speed) obj;
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
}
}
public static class CadenceAndSpeed extends SensorData {
private Cadence cadence;
private Speed speed;
public CadenceAndSpeed(String sensorAddress, String sensorName, @NonNull Cadence cadence, @NonNull Speed speed) {
super(sensorAddress, sensorName);
this.cadence = cadence;
this.speed = speed;
}
public Cadence getCadence() {
return cadence;
}
public Speed getSpeed() {
return speed;
}
}
}
@@ -0,0 +1,27 @@
package de.dennisguse.opentracks.content.sensor;
import androidx.annotation.NonNull;
public class SensorDataHeartRate extends SensorData {
private float heartRate_bpm;
public SensorDataHeartRate(String name, String address, float heartRate_bpm) {
super(name, address);
this.heartRate_bpm = heartRate_bpm;
}
public boolean hasHeartRate_bpm() {
return !Float.isNaN(heartRate_bpm);
}
public float getHeartRate_bpm() {
return heartRate_bpm;
}
@NonNull
@Override
public String toString() {
return "heart=" + heartRate_bpm;
}
}
@@ -1,94 +1,86 @@
package de.dennisguse.opentracks.content.sensor;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.data.TrackPoint;
/**
*
*/
public final class SensorDataSet {
public static final float DATA_UNAVAILABLE = Float.NaN;
private SensorDataHeartRate heartRate;
//TODO It might be necessary to consider: if sensor was connected as well.
private String sensorName;
private String sensorAddress;
private float heartRate;
private float cadence;
private float power;
private float batteryLevel;
private long creationTimestamp;
private SensorDataCycling.Cadence cyclingCadence;
public SensorDataSet(float heartRate, float cadence, float power, float batteryLevel, long creationTimestamp) {
this.heartRate = heartRate;
this.cadence = cadence;
this.power = power;
this.batteryLevel = batteryLevel;
this.creationTimestamp = creationTimestamp;
private SensorDataCycling.Speed cyclingSpeed;
public SensorDataSet() {
}
public SensorDataSet(float heartRate, float cadence, float power, float batteryLevel) {
this(heartRate, cadence, power, batteryLevel, System.currentTimeMillis());
}
public SensorDataSet(float heartRate, float cadence, float power) {
this(heartRate, cadence, power, DATA_UNAVAILABLE, System.currentTimeMillis());
}
public SensorDataSet(float heartRate, float cadence) {
this(heartRate, cadence, DATA_UNAVAILABLE, DATA_UNAVAILABLE, System.currentTimeMillis());
}
public SensorDataSet(float heartRate, String sensorName, String sensorAddress) {
this(heartRate, DATA_UNAVAILABLE, DATA_UNAVAILABLE, DATA_UNAVAILABLE, System.currentTimeMillis());
this.sensorName = sensorName;
this.sensorAddress = sensorAddress;
}
public boolean hasHeartRate() {
return !Float.isNaN(heartRate) && heartRate > 0;
}
public float getHeartRate() {
public SensorDataHeartRate getHeartRate() {
return heartRate;
}
public boolean hasCadence() {
return !Float.isNaN(cadence);
public SensorDataCycling.Cadence getCyclingCadence() {
return cyclingCadence;
}
public float getCadence() {
return cadence;
public SensorDataCycling.Speed getCyclingSpeed() {
return cyclingSpeed;
}
public boolean hasPower() {
return !Float.isNaN(power);
public void set(SensorData data) {
if (data == null) {
return;
}
if (data instanceof SensorDataHeartRate) {
this.heartRate = (SensorDataHeartRate) data;
return;
}
if (data instanceof SensorDataCycling.Cadence) {
this.cyclingCadence = (SensorDataCycling.Cadence) data;
return;
}
if (data instanceof SensorDataCycling.Speed) {
this.cyclingSpeed = (SensorDataCycling.Speed) data;
return;
}
if (data instanceof SensorDataCycling.CadenceAndSpeed) {
set(((SensorDataCycling.CadenceAndSpeed) data).getCadence());
set(((SensorDataCycling.CadenceAndSpeed) data).getSpeed());
}
throw new UnsupportedOperationException();
}
public float getPower() {
return power;
public void clear() {
this.heartRate = null;
this.cyclingCadence = null;
this.cyclingSpeed = null;
}
public long getCreationTime() {
return creationTimestamp;
public void fillTrackPoint(TrackPoint trackPoint) {
if (heartRate != null) {
trackPoint.setHeartRate_bpm(heartRate.getHeartRate_bpm());
}
if (cyclingCadence != null && cyclingCadence.hasCadence_rpm()) {
trackPoint.setCyclingCadence_rpm(cyclingCadence.getCadence_rpm());
}
if (cyclingSpeed != null && cyclingSpeed.hasSpeed_mps()) {
trackPoint.setSpeed(cyclingSpeed.getSpeed_mps());
}
}
/**
* Is the data recent considering the current time.
*
* @param maxAge the maximal age in milliseconds.
*/
public boolean isRecent(long maxAge) {
return creationTimestamp + maxAge > System.currentTimeMillis();
}
public boolean hasBatteryLevel() {
return !Float.isNaN(batteryLevel);
}
public float getBatteryLevel() {
return batteryLevel;
}
public String getSensorName() {
return sensorName;
}
public String getSensorAddress() {
return sensorAddress;
@NonNull
@Override
public String toString() {
return (getHeartRate() != null ? "" + getHeartRate() : "")
+ (getCyclingCadence() != null ? " " + getCyclingCadence() : "")
+ (getCyclingSpeed() != null ? " " + getCyclingSpeed() : "");
}
}
@@ -1,29 +0,0 @@
package de.dennisguse.opentracks.content.sensor;
import android.content.Context;
import de.dennisguse.opentracks.R;
public enum SensorState {
NONE,
CONNECTING,
CONNECTED,
DISCONNECTING,
DISCONNECTED;
public static String getStateAsString(SensorState state, Context c) {
switch (state) {
case NONE:
return c.getString(R.string.value_none);
case CONNECTING:
return c.getString(R.string.sensor_state_connecting);
case CONNECTED:
return c.getString(R.string.sensor_state_connected);
case DISCONNECTING:
case DISCONNECTED:
return c.getString(R.string.sensor_state_disconnected);
default:
return "";
}
}
}
@@ -75,6 +75,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
//TODO Why is this needed?
private int recordingDistanceInterval;
private String category = "";
// Modes of operation
private boolean chartByDistance;
@@ -100,7 +101,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
if (reportSpeed != chartView.getReportSpeed()) {
chartView.setReportSpeed(reportSpeed);
chartView.applyReportSpeed();
@@ -193,8 +194,16 @@ public class ChartFragment extends Fragment implements TrackDataListener {
if (isResumed()) {
if (track == null || track.getTrackStatistics() == null) {
startTime = -1L;
category = "";
return;
}
category = track.getCategory();
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
if (reportSpeed != chartView.getReportSpeed()) {
chartView.setReportSpeed(reportSpeed);
chartView.applyReportSpeed();
}
startTime = track.getTrackStatistics().getStartTime_ms();
}
}
@@ -313,7 +322,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
@VisibleForTesting
ChartPoint createPendingPoint(@NonNull TrackPoint trackPoint) {
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
return new ChartPoint(trackStatisticsUpdater, trackPoint.getSensorDataSet(), chartByDistance, chartView.getMetricUnits());
return new ChartPoint(trackStatisticsUpdater, trackPoint, chartByDistance, chartView.getMetricUnits());
}
@VisibleForTesting
@@ -85,7 +85,7 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
public void run() {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(fragmentActivity);
for (long markerId : markerIds) {
contentProviderUtils.deleteWaypoint(markerId);
contentProviderUtils.deleteWaypoint(getContext(), markerId);
}
caller.onDeleteMarkerDone();
}
@@ -43,12 +43,12 @@ import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackPointUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
@@ -113,9 +113,12 @@ public class StatsFragment extends Fragment implements TrackDataListener {
}
/* Views */
private View sensorContainerView;
private View heartRateContainer;
private TextView heartRateValueView;
private TextView heartRateSensorView;
private View cadenceContainer;
private TextView cadenceValueView;
private TextView cadenceSensorView;
private TextView totalTimeValueView;
private final Runnable updateUIeachSecond = new Runnable() {
@@ -161,10 +164,14 @@ public class StatsFragment extends Fragment implements TrackDataListener {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
sensorContainerView = view.findViewById(R.id.stats_sensor_container);
heartRateContainer = view.findViewById(R.id.stats_sensor_heart_rate_container);
heartRateValueView = view.findViewById(R.id.stats_sensor_heart_rate_value);
heartRateSensorView = view.findViewById(R.id.stats_sensor_heart_rate_sensor_value);
cadenceContainer = view.findViewById(R.id.stats_sensor_cadence_container);
cadenceValueView = view.findViewById(R.id.stats_sensor_cadence_value);
cadenceSensorView = view.findViewById(R.id.stats_sensor_cadence_sensor_value);
totalTimeValueView = view.findViewById(R.id.stats_total_time_value);
distanceValue = view.findViewById(R.id.stats_distance_value);
@@ -268,9 +275,12 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void onDestroyView() {
super.onDestroyView();
sensorContainerView = null;
heartRateContainer = null;
heartRateValueView = null;
heartRateSensorView = null;
cadenceContainer = null;
cadenceValueView = null;
cadenceSensorView = null;
totalTimeValueView = null;
@@ -356,7 +366,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
if (lastTrackPoint != null) {
boolean hasFix = !LocationUtils.isLocationOld(lastTrackPoint.getLocation());
boolean hasGoodFix = lastTrackPoint.hasAccuracy() && lastTrackPoint.getAccuracy() < recordingGpsAccuracy;
boolean hasGoodFix = TrackPointUtils.fulfillsAccuracy(lastTrackPoint, recordingGpsAccuracy);
if (!hasFix || !hasGoodFix) {
lastTrackPoint = null;
@@ -425,7 +435,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
}
/**
* Tries to fetch most recent {@link SensorDataSet} {@link de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager}.
* Tries to fetch most recent {@link SensorDataSet} from {@link de.dennisguse.opentracks.services.TrackRecordingService}.
*/
private void updateSensorDataUI() {
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
@@ -434,33 +444,65 @@ public class StatsFragment extends Fragment implements TrackDataListener {
if (trackRecordingService == null) {
Log.d(STATS_FRAGMENT_TAG, "Cannot get the track recording service.");
} else {
//TODO sensorState = trackRecordingService.getSensorState();
sensorDataSet = trackRecordingService.getSensorData();
}
setHeartRateSensorData(sensorDataSet, isSelectedTrackRecording());
setCadenceSensorData(sensorDataSet, isSelectedTrackRecording());
setSpeedSensorData(sensorDataSet, isSelectedTrackRecording());
}
private void setHeartRateSensorData(SensorDataSet sensorDataSet, boolean isRecording) {
// heart rate
int isVisible = View.VISIBLE;
if (!isRecording || PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(getContext())) {
if (!isRecording || PreferencesUtils.isBluetoothHeartRateSensorAddressNone(getContext())) {
isVisible = View.INVISIBLE;
}
sensorContainerView.setVisibility(isVisible);
heartRateContainer.setVisibility(isVisible);
if (isRecording) {
String heartRate = getContext().getString(R.string.value_unknown);
String sensorValue = getContext().getString(R.string.value_unknown);
String sensorName = getContext().getString(R.string.value_unknown);
if (sensorDataSet != null && sensorDataSet.isRecent(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS)) {
sensorName = sensorDataSet.getSensorName();
if (sensorDataSet.hasHeartRate()) {
heartRate = StringUtils.formatDecimal(sensorDataSet.getHeartRate(), 0);
if (sensorDataSet != null && sensorDataSet.getHeartRate() != null) {
sensorName = sensorDataSet.getHeartRate().getSensorName();
if (sensorDataSet.getHeartRate().hasHeartRate_bpm() && sensorDataSet.getHeartRate().isRecent()) {
sensorValue = StringUtils.formatDecimal(sensorDataSet.getHeartRate().getHeartRate_bpm(), 0);
}
}
heartRateSensorView.setText(sensorName);
heartRateValueView.setText(heartRate);
heartRateValueView.setText(sensorValue);
}
}
private void setCadenceSensorData(SensorDataSet sensorDataSet, boolean isRecording) {
int isVisible = View.VISIBLE;
if (!isRecording || PreferencesUtils.isBluetoothCyclingCadenceSensorAddressNone(getContext())) {
isVisible = View.INVISIBLE;
}
cadenceContainer.setVisibility(isVisible);
if (isRecording) {
String sensorValue = getContext().getString(R.string.value_unknown);
String sensorName = getContext().getString(R.string.value_unknown);
if (sensorDataSet != null && sensorDataSet.getCyclingCadence() != null) {
sensorName = sensorDataSet.getCyclingCadence().getSensorName();
if (sensorDataSet.getCyclingCadence().hasCadence_rpm() && sensorDataSet.getCyclingCadence().isRecent()) {
sensorValue = StringUtils.formatDecimal(sensorDataSet.getCyclingCadence().getCadence_rpm(), 0);
}
}
cadenceSensorView.setText(sensorName);
cadenceValueView.setText(sensorValue);
}
}
private void setSpeedSensorData(SensorDataSet sensorDataSet, boolean isRecording) {
if (isRecording) {
if (sensorDataSet != null && sensorDataSet.getCyclingSpeed() != null) {
if (sensorDataSet.getCyclingSpeed().hasSpeed_mps() && sensorDataSet.getCyclingSpeed().isRecent()) {
setSpeed(sensorDataSet.getCyclingSpeed().getSpeed_mps());
}
}
}
}
@@ -468,7 +510,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
boolean isRecording = isSelectedTrackRecording();
// Set total distance
@@ -561,18 +603,13 @@ public class StatsFragment extends Fragment implements TrackDataListener {
private void setLocationValues() {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext());
boolean isRecording = isSelectedTrackRecording();
// Set speed/pace
speedContainer.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
speedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
double speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : Double.NaN;
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
speedValue.setText(parts.first);
speedUnit.setText(parts.second);
setSpeed(speed);
}
// Set elevation
@@ -602,4 +639,15 @@ public class StatsFragment extends Fragment implements TrackDataListener {
longitudeValue.setText(longitudeText);
}
}
private void setSpeed(double speed) {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
speedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
speedValue.setText(parts.first);
speedUnit.setText(parts.second);
}
}
@@ -24,7 +24,6 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
@@ -43,7 +42,7 @@ import de.dennisguse.opentracks.util.FileUtils;
*/
public class ExportActivity extends FragmentActivity implements FileTypeDialogFragment.FileTypeCaller {
private static final int EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 6;
private static final int DIRECTORY_PICKER_REQUEST_CODE = 6;
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
@@ -64,18 +63,17 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(intent, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
startActivityForResult(intent, DIRECTORY_PICKER_REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
if (requestCode == DIRECTORY_PICKER_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
directoryUri = resultData.getData();
fileTypeDialogStart();
} else {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
}
}
@@ -108,7 +106,7 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
switch (id) {
case DIALOG_PROGRESS_ID:
progressDialog = DialogUtils.createHorizontalProgressDialog(this,
R.string.export_external_storage_progress_message,
R.string.export_progress_message,
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
@@ -128,11 +126,11 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
iconId = R.drawable.ic_dialog_success_24dp;
titleId = R.string.generic_success_title;
message = getString(
R.string.export_external_storage_success, totalTracks, directoryDisplayName);
R.string.export_success, totalTracks, directoryDisplayName);
} else {
iconId = R.drawable.ic_dialog_error_24dp;
titleId = R.string.generic_error_title;
message = getString(R.string.export_external_storage_error, processedTrackCount, totalTracks, directoryDisplayName);
message = getString(R.string.export_error, processedTrackCount, totalTracks, directoryDisplayName);
}
AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(true)
.setIcon(iconId).setMessage(message)
@@ -18,7 +18,6 @@ package de.dennisguse.opentracks.io.file.exporter;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import java.io.OutputStream;
import java.io.PrintWriter;
@@ -31,7 +30,7 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -72,11 +71,11 @@ public class KmlTrackWriter implements TrackWriter {
private TrackPoint startTrackPoint;
/**
* @param context the context
* @param hasMultipleTracks should encode multiple tracks into one file?
* @param exportTrackDetail should detailed information about the track be exported (e.g., title, description, waypoints, timing)?
* @param exportSensorData should {@link SensorDataSet} be exported?
* @param exportPhotos should pictures be exported (if true: exports to KMZ)?
* @param context the context
* @param hasMultipleTracks should encode multiple tracks into one file?
* @param exportTrackDetail should detailed information about the track be exported (e.g., title, description, waypoints, timing)?
* @param exportSensorData should {@link TrackPoint}'s sensor data be exported?
* @param exportPhotos should pictures be exported (if true: exports to KMZ)?
*/
public KmlTrackWriter(Context context, boolean hasMultipleTracks, boolean exportTrackDetail, boolean exportSensorData, boolean exportPhotos) {
this.context = context;
@@ -162,9 +161,10 @@ public class KmlTrackWriter implements TrackWriter {
@Override
public void writeWaypoint(Waypoint waypoint) {
if (printWriter != null && exportTrackDetail) {
if (waypoint.hasPhoto() && exportPhotos) {
boolean existsPhoto = FileUtils.getPhotoFileIfExists(context, waypoint.getTrackId(), waypoint.getPhotoURI()) != null;
if (waypoint.hasPhoto() && exportPhotos && existsPhoto) {
float heading = getHeading(waypoint.getTrackId(), waypoint.getLocation());
writePhotoOverlay(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), waypoint.getLocation(), waypoint.getPhotoUrl(), heading);
writePhotoOverlay(waypoint, heading);
} else {
writePlacemark(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), WAYPOINT_STYLE, waypoint.getLocation());
}
@@ -267,17 +267,14 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.println("<gx:coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</gx:coord>");
if (exportSensorData) {
SensorDataSet sensorDataSet = trackPoint.getSensorDataSet();
if (sensorDataSet != null) {
if (sensorDataSet.hasHeartRate()) {
heartRateList.add(sensorDataSet.getHeartRate());
}
if (sensorDataSet.hasCadence()) {
cadenceList.add(sensorDataSet.getCadence());
}
if (sensorDataSet.hasPower()) {
powerList.add(sensorDataSet.getPower());
}
if (trackPoint.hasHeartRate()) {
heartRateList.add(trackPoint.getHeartRate_bpm());
}
if (trackPoint.hasCyclingCadence()) {
cadenceList.add(trackPoint.getCyclingCadence_rpm());
}
if (trackPoint.hasPower()) {
powerList.add(trackPoint.getPower());
}
}
}
@@ -322,32 +319,29 @@ public class KmlTrackWriter implements TrackWriter {
}
/**
* Writes a photo overlay.
* @param name the name
* @param category the category
* @param description the description
* @param location the location
* @param photoUrl the photo url
* @param heading the heading
* Writes a photo overlay from waypoint.
*
* @param waypoint Waypoint object.
* @param heading the heading.
*/
private void writePhotoOverlay(String name, String category, String description, Location location, String photoUrl, float heading) {
if (location != null && exportTrackDetail) {
private void writePhotoOverlay(Waypoint waypoint, float heading) {
if (exportTrackDetail) {
printWriter.println("<PhotoOverlay>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println("<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.println("<name>" + StringUtils.formatCData(waypoint.getName()) + "</name>");
printWriter.println("<description>" + StringUtils.formatCData(waypoint.getDescription()) + "</description>");
printWriter.print("<Camera>");
printWriter.print("<longitude>" + location.getLongitude() + "</longitude>");
printWriter.print("<latitude>" + location.getLatitude() + "</latitude>");
printWriter.print("<longitude>" + waypoint.getLocation().getLongitude() + "</longitude>");
printWriter.print("<latitude>" + waypoint.getLocation().getLatitude() + "</latitude>");
printWriter.print("<altitude>20</altitude>");
printWriter.print("<heading>" + heading + "</heading>");
printWriter.print("<tilt>90</tilt>");
printWriter.println("</Camera>");
printWriter.println("<TimeStamp><when>" + getTime(location) + "</when></TimeStamp>");
printWriter.println("<TimeStamp><when>" + getTime(waypoint.getLocation()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + KmlTrackWriter.WAYPOINT_STYLE + "</styleUrl>");
writeCategory(category);
writeCategory(waypoint.getCategory());
if (exportPhotos) {
printWriter.println("<Icon><href>" + KmzTrackExporter.buildKmzImageFilePath(Uri.parse(photoUrl)) + "</href></Icon>");
printWriter.println("<Icon><href>" + KmzTrackExporter.buildKmzImageFilePath(waypoint) + "</href></Icon>");
}
printWriter.print("<ViewVolume>");
@@ -358,7 +352,7 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.print("<topFov>45</topFov>");
printWriter.println("</ViewVolume>");
printWriter.println("<Point>");
printWriter.println("<coordinates>" + getCoordinates(location, ",") + "</coordinates>");
printWriter.println("<coordinates>" + getCoordinates(waypoint.getLocation(), ",") + "</coordinates>");
printWriter.println("</Point>");
printWriter.println("</PhotoOverlay>");
}
@@ -454,7 +448,7 @@ public class KmlTrackWriter implements TrackWriter {
/**
* Writes a sensor style.
*
* @param name the name of the sesnor
* @param name the name of the sesnor
* @param sensorType the sensor display name
*/
private void writeSensorStyle(String name, String sensorType) {
@@ -42,7 +42,7 @@ import de.dennisguse.opentracks.util.FileUtils;
* @author Jimmy Shih
*/
public class KmzTrackExporter implements TrackExporter {
public static final String KMZ_IMAGES_DIR = "images";
private static final String KMZ_IMAGES_DIR = "images";
public static final String KMZ_KML_FILE = "doc.kml";
private static final String TAG = KmzTrackExporter.class.getSimpleName();
@@ -112,7 +112,11 @@ public class KmzTrackExporter implements TrackExporter {
}
Waypoint waypoint = contentProviderUtils.createWaypoint(cursor);
if (waypoint.hasPhoto()) {
addImage(context, zipOutputStream, waypoint.getPhotoUrl());
Uri uriPhoto = waypoint.getPhotoURI();
boolean existsPhoto = FileUtils.getPhotoFileIfExists(context, track.getId(), uriPhoto) != null;
if (existsPhoto) {
addImage(context, zipOutputStream, uriPhoto, waypoint);
}
}
cursor.moveToNext();
@@ -122,11 +126,9 @@ public class KmzTrackExporter implements TrackExporter {
}
}
private void addImage(Context context, ZipOutputStream zipOutputStream, String photoUrl) throws IOException {
Uri uri = Uri.parse(photoUrl);
private void addImage(Context context, ZipOutputStream zipOutputStream, Uri uri, Waypoint waypoint) throws IOException {
try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) {
ZipEntry zipEntry = new ZipEntry(buildKmzImageFilePath(uri));
ZipEntry zipEntry = new ZipEntry(buildKmzImageFilePath(waypoint));
zipOutputStream.putNextEntry(zipEntry);
if (inputStream == null) throw new FileNotFoundException();
@@ -149,11 +151,13 @@ public class KmzTrackExporter implements TrackExporter {
}
/**
* Builds and returns the path for the file that will be saved inside KMZ_IMAGES_DIR.
* Builds and returns the path for the image that will be saved inside KMZ_IMAGES_DIR for the waypoint.
*
* @param uri URI object.
* @param waypoint Waypoint object.
*/
public static String buildKmzImageFilePath(Uri uri) {
return KMZ_IMAGES_DIR + File.separatorChar + FileUtils.sanitizeFileName(uri.getLastPathSegment());
public static String buildKmzImageFilePath(Waypoint waypoint) {
String ext = FileUtils.getExtension(waypoint.getPhotoUrl());
ext = ext == null ? "" : "." + ext;
return KMZ_IMAGES_DIR + File.separatorChar + FileUtils.sanitizeFileName(waypoint.getId() + ext);
}
}
@@ -28,6 +28,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
@@ -312,6 +313,9 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
*/
protected TrackPoint getTrackPoint() throws SAXException {
TrackPoint trackPoint = createTrackPoint();
if (trackPoint == null) {
throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
}
// Calculate derived attributes from the previous point
if (trackData.lastLocationInCurrentSegment != null && trackData.lastLocationInCurrentSegment.getTime() != 0) {
@@ -379,23 +383,15 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
return null;
}
Uri externalPhotoUri = Uri.parse(externalPhotoUrl);
if (externalPhotoUri == null) {
Log.w(TAG, "Could not parse external photo url.");
String importFileName = KmzTrackImporter.importNameForFilename(externalPhotoUrl);
File file = FileUtils.getPhotoFileIfExists(context, importTrackId, Uri.parse(importFileName));
if (file != null) {
Uri photoUri = FileUtils.getUriForFile(context, file);
return "" + photoUri;
}
else {
return null;
}
String filename = externalPhotoUri.getLastPathSegment();
if (filename == null) {
Log.w(TAG, "External photo contains no filename.");
return null;
}
File dir = FileUtils.getPhotoDir(context, importTrackId);
File file = new File(dir, externalPhotoUrl);
Uri photoUri = FileUtils.getUriForFile(context, file);
return "" + photoUri;
}
/**
@@ -446,11 +442,11 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
}
trackData.trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
trackData.bufferedTrackPoints[trackData.numBufferedLocations] = trackPoint;
trackData.numBufferedLocations++;
trackData.bufferedTrackPoints[trackData.numBufferedTrackPoints] = trackPoint;
trackData.numBufferedTrackPoints++;
trackData.numberOfLocations++;
if (trackData.numBufferedLocations >= MAX_BUFFERED_LOCATIONS) {
if (trackData.numBufferedTrackPoints >= MAX_BUFFERED_LOCATIONS) {
flushLocations(trackData);
}
}
@@ -461,11 +457,11 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* @param data the track data
*/
private void flushLocations(TrackData data) {
if (data.numBufferedLocations <= 0) {
if (data.numBufferedTrackPoints <= 0) {
return;
}
contentProviderUtils.bulkInsertTrackPoint(data.bufferedTrackPoints, data.numBufferedLocations, data.track.getId());
data.numBufferedLocations = 0;
contentProviderUtils.bulkInsertTrackPoint(Arrays.copyOfRange(data.bufferedTrackPoints, 0, data.numBufferedTrackPoints), data.track.getId());
data.numBufferedTrackPoints = 0;
}
/**
@@ -506,6 +502,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
final TrackPoint[] bufferedTrackPoints = new TrackPoint[MAX_BUFFERED_LOCATIONS];
// The number of buffered locations
int numBufferedLocations = 0;
int numBufferedTrackPoints = 0;
}
}
@@ -24,7 +24,6 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
@@ -43,7 +42,7 @@ import de.dennisguse.opentracks.util.FileUtils;
*/
public class ImportActivity extends FragmentActivity implements FileTypeDialogFragment.FileTypeCaller {
private static final int EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 6;
private static final int DIRECTORY_PICKER_REQUEST_CODE = 6;
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
@@ -65,18 +64,17 @@ public class ImportActivity extends FragmentActivity implements FileTypeDialogFr
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(intent, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
startActivityForResult(intent, DIRECTORY_PICKER_REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
if (requestCode == DIRECTORY_PICKER_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
directoryUri = resultData.getData();
fileTypeDialogStart();
} else {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
}
}
@@ -16,7 +16,6 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
@@ -46,39 +45,23 @@ class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ImportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat;
private final DocumentFile directory;
// TODO Can be removed, or?
private Context context;
// TODO Use weak reference
private ImportActivity importActivity;
private WakeLock wakeLock;
// true if the AsyncTask has completed
private boolean completed;
private int importTrackCount;
private int totalTrackCount;
private long lastSuccessfulTrackId;
// the last successfully imported track id
private long trackId;
/**
* Creates an AsyncTask.
*
* @param importActivity the activity currently associated with this AsyncTask
* @param trackFileFormat the track file format
* @param directory path to import GPX files
*/
public ImportAsyncTask(ImportActivity importActivity, TrackFileFormat trackFileFormat, DocumentFile directory) {
this.importActivity = importActivity;
this.trackFileFormat = trackFileFormat;
this.directory = directory;
context = importActivity.getApplicationContext();
completed = false;
importTrackCount = 0;
totalTrackCount = 0;
trackId = -1L;
lastSuccessfulTrackId = -1L;
}
@Override
@@ -90,8 +73,6 @@ class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
/**
* Gets a list of files.
* If importAll is true, returns a list of the files under the path directory.
* If importAll is false, returns a list containing just the path file.
*/
private static List<DocumentFile> getFiles(DocumentFile file, TrackFileFormat trackFileFormat) {
List<DocumentFile> files = new ArrayList<>();
@@ -99,7 +80,7 @@ class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
for (DocumentFile candidate : file.listFiles()) {
if (!candidate.isDirectory()) {
String extension = FileUtils.getExtension(candidate.getName());
if (trackFileFormat.getExtension().equals(extension)) {
if (extension != null && trackFileFormat.getExtension().equals(extension)) {
files.add(candidate);
}
} else {
@@ -119,7 +100,6 @@ class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override
protected void onPostExecute(Boolean result) {
completed = true;
if (importActivity != null) {
importActivity.onAsyncTaskCompleted(importTrackCount, totalTrackCount);
}
@@ -127,8 +107,6 @@ class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override
protected void onCancelled() {
context = null;
completed = true;
if (importActivity != null) {
importActivity.onAsyncTaskCompleted(importTrackCount, totalTrackCount);
importActivity = null;
@@ -186,13 +164,13 @@ class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
Uri uri = contentProviderUtils.insertTrack(new Track());
long newId = Long.parseLong(uri.getLastPathSegment());
trackImporter = new KmzTrackImporter(importActivity, newId);
trackImporter = new KmzTrackImporter(importActivity, newId, file.getUri());
}
}
try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) {
trackId = trackImporter.importFile(inputStream);
return trackId != -1L;
try (InputStream inputStream = importActivity.getContentResolver().openInputStream(file.getUri())) {
lastSuccessfulTrackId = trackImporter.importFile(inputStream);
return lastSuccessfulTrackId != -1L;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
return false;
@@ -28,7 +28,6 @@ import java.util.ArrayList;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.io.file.exporter.KmlTrackWriter;
/**
@@ -223,30 +222,17 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
for (int i = 0; i < trackPoints.size(); i++) {
TrackPoint trackPoint = trackPoints.get(i);
boolean hasSensorData = false;
float heartrate = SensorDataSet.DATA_UNAVAILABLE;
float cadence = SensorDataSet.DATA_UNAVAILABLE;
float power = SensorDataSet.DATA_UNAVAILABLE;
if (i < heartRateList.size()) {
heartrate = heartRateList.get(i);
hasSensorData = true;
trackPoint.setHeartRate_bpm(heartRateList.get(i));
}
if (i < cadenceList.size()) {
cadence = cadenceList.get(i);
hasSensorData = true;
trackPoint.setCyclingCadence_rpm(cadenceList.get(i));
}
if (i < powerList.size()) {
power = powerList.get(i);
hasSensorData = true;
trackPoint.setPower(cadenceList.get(i));
}
if (!hasSensorData) {
insertTrackPoint(trackPoint);
} else {
TrackPoint sensorDataSetLocation = new TrackPoint(trackPoint, new SensorDataSet(heartrate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, trackPoint.getTime()));
insertTrackPoint(sensorDataSetLocation);
}
insertTrackPoint(trackPoint);
}
}
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import java.io.ByteArrayInputStream;
@@ -25,9 +26,13 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
import de.dennisguse.opentracks.util.FileUtils;
@@ -42,58 +47,185 @@ public class KmzTrackImporter implements TrackImporter {
private static final String TAG = KmzTrackImporter.class.getSimpleName();
private static final List<String> KMZ_IMAGES_EXT = Arrays.asList("jpeg", "jpg", "png");
private static final int BUFFER_SIZE = 4096;
private final Context context;
private final long importTrackId;
private Uri uriKmzFile;
/**
* Constructor.
*
* @param context the context
* @param importTrackId track id to import to. This should not be -1L so that images in the kmz file can be imported.
* @param uriFile URI of the kmz file.
*/
KmzTrackImporter(Context context, long importTrackId) {
KmzTrackImporter(Context context, long importTrackId, Uri uriFile) {
this.context = context;
this.importTrackId = importTrackId;
this.uriKmzFile = uriFile;
}
@Override
public long importFile(InputStream inputStream) {
long trackId = importTrackId;
try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
long trackId;
if (!copyKmzImages()) {
cleanImport(context, importTrackId);
return -1L;
}
trackId = findAndParseKmlFile(inputStream);
if (trackId == -1L) {
cleanImport(context, importTrackId);
return -1L;
}
deleteOrphanImages(context, trackId);
return trackId;
}
/**
* Copies all images that are inside KMZ to OpenTracks external storage.
*
* @return false if there are errors or true otherwise.
*/
private boolean copyKmzImages() {
try (InputStream inputStream = context.getContentResolver().openInputStream(uriKmzFile);
ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
if (Thread.interrupted()) {
Log.d(TAG, "Thread interrupted");
cleanImport(context, trackId);
return false;
}
String fileName = zipEntry.getName();
if (hasImageExtension(fileName)) {
readAndSaveImageFile(zipInputStream, importNameForFilename(fileName));
}
zipInputStream.closeEntry();
}
return true;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
return false;
}
}
/**
* From path fileName generates an import unique name and returns it.
* The name generator is simple: change the path fileName with '-' instead of File.separatorChar.
*
* @param fileName the file name.
*/
public static String importNameForFilename(String fileName) {
// TODO this tricky code for maintain backward compatibility must be deleted some day.
/*
* In versions before v3.5.0 photo URL in KML files were wrong.
* For compatibility reasons it checks if fileName begins with "content://" or "file://".
* All fileName begins with "content:/" or "file://" are cooked.
* We cannot guess what's the folder name where images are so we use "images" that was the folder name expected in versions before v3.5.0.
*/
if (fileName.startsWith("content://") || fileName.startsWith("file://")) {
fileName = "images/" + fileName.substring(fileName.lastIndexOf(File.separatorChar) + 1);
}
return fileName.replace(File.separatorChar, '-');
}
/**
* Returns true if fileName ends with some of the KMZ_IMAGES_EXT suffixes.
* Otherwise returns false.
*/
private boolean hasImageExtension(String fileName) {
if (fileName == null) {
return false;
}
String fileExt = FileUtils.getExtension(fileName.toLowerCase());
if (fileExt == null) {
return false;
}
return KMZ_IMAGES_EXT.contains(fileExt);
}
/**
* Finds KmzTrackExporter.KMZ_KML_FILE file inside kmz file (inputStream) and it parses it.
*
* @param inputStream kmz input stream.
* @return -1 if error or the id of the track otherwise.
*/
private long findAndParseKmlFile(InputStream inputStream) {
try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
ZipEntry zipEntry;
long trackId = -1L;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
if (Thread.interrupted()) {
Log.d(TAG, "Thread interrupted");
return -1L;
}
String fileName = zipEntry.getName();
if (KmzTrackExporter.KMZ_KML_FILE.equals(fileName)) {
trackId = parseKml(zipInputStream);
if (trackId == -1L) {
Log.d(TAG, "Unable to parse kml in kmz");
cleanImport(context, trackId);
return -1L;
}
} else {
String prefix = KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar;
if (fileName.startsWith(prefix)) {
readImageFile(zipInputStream, fileName.substring(prefix.length()));
}
}
zipInputStream.closeEntry();
}
return trackId;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
cleanImport(context, trackId);
return -1L;
}
}
/**
* Deletes all images that remained in external storage that doesn't have waypoint (marker) associated.
*
* @param context the Context object.
* @param trackId the id of the Track.
*/
private void deleteOrphanImages(Context context, long trackId) {
if (trackId != 1L) {
// 1.- Gets all photo names in the waypoints of the track identified by id.
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
List<Waypoint> waypoints = contentProviderUtils.getWaypoints(trackId);
List<String> photosName = new ArrayList<>();
for (Waypoint w : waypoints) {
if (w.hasPhoto()) {
String photoUrl = Uri.decode(w.getPhotoUrl());
photosName.add(photoUrl.substring(photoUrl.lastIndexOf(File.separatorChar) + 1));
}
}
// 2.- Deletes all orphan photos from external storage.
File dir = FileUtils.getPhotoDir(context, trackId);
if (dir.exists() && dir.isDirectory()) {
for (File file : dir.listFiles()) {
if (!photosName.contains(file.getName())) {
file.delete();
}
}
if (dir.listFiles().length == 0) {
dir.delete();
}
}
}
}
/**
* Cleans up import.
*
@@ -104,16 +236,6 @@ public class KmzTrackImporter implements TrackImporter {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
contentProviderUtils.deleteTrack(context, trackId);
}
if (importTrackId != -1L) {
File dir = FileUtils.getPhotoDir(context, importTrackId);
if (dir.exists() && dir.isDirectory()) {
for (File file : dir.listFiles()) {
file.delete();
}
dir.delete();
}
}
}
/**
@@ -147,12 +269,12 @@ public class KmzTrackImporter implements TrackImporter {
}
/**
* Reads an image file.
* Reads an image file (zipInputStream) and save it in a file called fileName inside photo folder.
*
* @param zipInputStream the zip input stream
* @param fileName the file name
*/
private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException {
private void readAndSaveImageFile(ZipInputStream zipInputStream, String fileName) throws IOException {
if (importTrackId == -1L || fileName.equals("")) {
return;
}
@@ -33,6 +33,8 @@ import android.os.IBinder;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.TaskStackBuilder;
import java.util.concurrent.ExecutorService;
@@ -59,6 +61,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
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;
/**
@@ -69,13 +72,11 @@ import de.dennisguse.opentracks.util.UnitConversions;
*/
public class TrackRecordingService extends Service {
// Anything faster than that (in meters per second) will be considered moving.
private static final String TAG = TrackRecordingService.class.getSimpleName();
public static final double MAX_NO_MOVEMENT_SPEED = 0.224;
// The following variables are set in onCreate:
private ExecutorService executorService;
@Deprecated //TODO Should not be necessary
private ExecutorService locationExecutorService; // Enforces order of location changes.
private ContentProviderUtils contentProviderUtils;
private LocationManager locationManager;
private PeriodicTaskExecutor voiceExecutor;
@@ -139,7 +140,6 @@ public class TrackRecordingService extends Service {
private TrackStatisticsUpdater trackStatisticsUpdater;
private TrackPoint lastTrackPoint;
private boolean currentSegmentHasLocation;
private boolean isIdle;
private TrackRecordingServiceBinder binder = new TrackRecordingServiceBinder(this);
@@ -147,10 +147,10 @@ public class TrackRecordingService extends Service {
@Override
public void onLocationChanged(final Location location) {
if (executorService == null || executorService.isShutdown() || executorService.isTerminated()) {
if (locationExecutorService == null || locationExecutorService.isShutdown() || locationExecutorService.isTerminated()) {
return;
}
executorService.submit(new Runnable() {
locationExecutorService.submit(new Runnable() {
@Override
public void run() {
onLocationChangedAsync(location);
@@ -177,7 +177,7 @@ public class TrackRecordingService extends Service {
@Override
public void onCreate() {
super.onCreate();
executorService = Executors.newSingleThreadExecutor();
locationExecutorService = Executors.newSingleThreadExecutor();
contentProviderUtils = new ContentProviderUtils(this);
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory());
@@ -190,17 +190,7 @@ public class TrackRecordingService extends Service {
PreferencesUtils.register(this, sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
// Try to restart the previous recording track in case the service has been restarted by the system, which can sometimes happen.
Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track != null) {
restartTrack(track);
} else {
if (isRecording()) {
Log.w(TAG, "track is null, but recordingTrackId not -1L. " + recordingTrackId);
updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
}
showNotification(false);
}
restartTrackAfterServiceRestart();
}
@Override
@@ -243,7 +233,7 @@ public class TrackRecordingService extends Service {
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
// Shutdown the executorService last to avoid sending events to a dead executor.
executorService.shutdown();
locationExecutorService.shutdown();
super.onDestroy();
}
@@ -367,8 +357,8 @@ public class TrackRecordingService extends Service {
track.getTrackStatistics().setStopTime_ms(System.currentTimeMillis());
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics());
insertTrackPoint(track, TrackPoint.createPause(), null);
insertTrackPoint(track, TrackPoint.createResume(), null);
insertTrackPoint(track, TrackPoint.createPause());
insertTrackPoint(track, TrackPoint.createResume());
// Update shared preferences.
updateRecordingState(trackId, false);
@@ -376,13 +366,26 @@ public class TrackRecordingService extends Service {
startRecording();
}
private void restartTrack(Track track) {
/**
* Try to restart the previous recording track in case the service has been restarted by the system, which can sometimes happen.
*/
private void restartTrackAfterServiceRestart() {
Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track == null) {
if (isRecording()) {
Log.w(TAG, "track is null, but recordingTrackId not -1L. " + recordingTrackId);
updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
}
showNotification(false);
return;
}
Log.d(TAG, "Restarting track: " + track.getId());
trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics().getStartTime_ms());
try (TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false)) {
trackStatisticsUpdater.addTrackPoint(locationIterator, recordingDistanceInterval);
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false)) {
trackStatisticsUpdater.addTrackPoint(trackPointIterator, recordingDistanceInterval);
} catch (RuntimeException e) {
Log.e(TAG, "RuntimeException", e);
}
@@ -395,14 +398,12 @@ public class TrackRecordingService extends Service {
return;
}
// Update shared preferences
recordingTrackPaused = false;
PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, false);
updateRecordingState(recordingTrackId, false);
// Update database
Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track != null) {
insertTrackPoint(track, TrackPoint.createResume(), null);
insertTrackPoint(track, TrackPoint.createResume());
}
startRecording();
@@ -416,7 +417,6 @@ public class TrackRecordingService extends Service {
remoteSensorManager = new BluetoothRemoteSensorManager(this);
remoteSensorManager.start();
lastTrackPoint = null;
currentSegmentHasLocation = false;
isIdle = false;
startGps();
@@ -445,26 +445,21 @@ public class TrackRecordingService extends Service {
// Need to remember the recordingTrackId before setting it to -1L
long trackId = recordingTrackId;
boolean paused = recordingTrackPaused;
boolean wasPaused = recordingTrackPaused;
// Update shared preferences
updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
// Update database
Track track = contentProviderUtils.getTrack(trackId);
if (track != null) {
// If not paused, add the last location
if (!paused) {
insertTrackPoint(track, lastTrackPoint, getLastValidTrackPointInCurrentSegment(trackId));
// If not wasPaused, add the last location
if (!wasPaused) {
if (lastTrackPoint != null) {
insertTrackPointIfNewer(track, lastTrackPoint);
}
// Update the recording track time
updateRecordingTrack(track);
}
String trackName = TrackNameUtils.getTrackName(this, trackId, track.getTrackStatistics().getStartTime_ms());
if (trackName != null && !trackName.equals(track.getName())) {
track.setName(trackName);
contentProviderUtils.updateTrack(track);
updateTrackTotalTime(track);
}
}
endRecording(true);
@@ -476,16 +471,15 @@ public class TrackRecordingService extends Service {
return;
}
// Update shared preferences
recordingTrackPaused = true;
PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, true);
updateRecordingState(recordingTrackId, true);
// Update database
Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track != null) {
insertTrackPoint(track, lastTrackPoint, getLastValidTrackPointInCurrentSegment(track.getId()));
insertTrackPoint(track, TrackPoint.createPause(), null);
if (lastTrackPoint != null) {
insertTrackPointIfNewer(track, lastTrackPoint);
}
insertTrackPoint(track, TrackPoint.createPause());
}
endRecording(false);
@@ -535,14 +529,19 @@ public class TrackRecordingService extends Service {
* @return the location or null
*/
private TrackPoint getLastValidTrackPointInCurrentSegment(long trackId) {
if (!currentSegmentHasLocation) {
if (!currentSegmentHasTrackPoint()) {
return null;
}
return contentProviderUtils.getLastValidTrackPoint(trackId);
}
private boolean currentSegmentHasTrackPoint() {
return lastTrackPoint != null;
}
/**
* Updates the recording states.
* This will inform subscribed {@link OnSharedPreferenceChangeListener}.
*
* @param trackId the recording track id
* @param paused true if the recording is paused
@@ -571,93 +570,118 @@ public class TrackRecordingService extends Service {
return;
}
TrackPoint trackPoint = new TrackPoint(location, getSensorDataSet());
TrackPoint trackPoint = new TrackPoint(location);
fillWithSensorDataSet(trackPoint);
notificationManager.updateTrackPoint(this, trackPoint, recordingGpsAccuracy);
if (!location.hasAccuracy() || location.getAccuracy() >= recordingGpsAccuracy) {
if (!TrackPointUtils.fulfillsAccuracy(trackPoint, recordingGpsAccuracy)) {
Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy.");
return;
}
//TODO Necessary?
// Fix for phones that do not set the time field
if (location.getTime() == 0L) {
location.setTime(System.currentTimeMillis());
}
TrackPointUtils.fixTime(trackPoint);
//TODO Figure out how to avoid loading the lastValidTrackPoint from the database
TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
long idleTime = 0L;
if (lastValidTrackPoint != null && location.getTime() > lastValidTrackPoint.getLocation().getTime()) {
idleTime = location.getTime() - lastValidTrackPoint.getLocation().getTime();
if (TrackPointUtils.after(trackPoint, lastValidTrackPoint)) {
idleTime = trackPoint.getTime() - lastValidTrackPoint.getTime();
}
locationListenerPolicy.updateIdleTime(idleTime);
if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) {
registerLocationListener();
}
//Storing trackPoint
// Always insert the first segment location
if (!currentSegmentHasLocation) {
insertTrackPoint(track, trackPoint, null);
currentSegmentHasLocation = true;
if (!currentSegmentHasTrackPoint()) {
insertTrackPoint(track, trackPoint);
lastTrackPoint = trackPoint;
return;
}
if (!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, null);
insertTrackPoint(track, trackPoint);
lastTrackPoint = trackPoint;
return;
}
double distanceToLastTrackLocation = location.distanceTo(lastValidTrackPoint.getLocation());
double distanceToLastTrackLocation = trackPoint.distanceTo(lastValidTrackPoint);
if (distanceToLastTrackLocation > maxRecordingDistance) {
insertTrackPoint(track, lastTrackPoint, lastValidTrackPoint);
insertTrackPoint(track, TrackPoint.createPause(), null);
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, TrackPoint.createPause());
insertTrackPoint(track, trackPoint, null);
insertTrackPoint(track, trackPoint);
isIdle = false;
} else if (trackPoint.getSensorDataSet() != null || distanceToLastTrackLocation >= recordingDistanceInterval) {
insertTrackPoint(track, lastTrackPoint, lastValidTrackPoint);
insertTrackPoint(track, trackPoint, null);
isIdle = false;
} else if (!isIdle && location.hasSpeed() && location.getSpeed() < MAX_NO_MOVEMENT_SPEED) {
insertTrackPoint(track, lastTrackPoint, lastValidTrackPoint);
insertTrackPoint(track, trackPoint, null);
isIdle = true;
} else if (isIdle && location.hasSpeed() && location.getSpeed() >= MAX_NO_MOVEMENT_SPEED) {
insertTrackPoint(track, lastTrackPoint, lastValidTrackPoint);
insertTrackPoint(track, trackPoint, null);
isIdle = false;
} else {
Log.d(TAG, "Not recording location, idle");
lastTrackPoint = trackPoint;
return;
}
if (trackPoint.hasSensorData() || distanceToLastTrackLocation >= recordingDistanceInterval) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
isIdle = false;
lastTrackPoint = trackPoint;
return;
}
if (!isIdle && !TrackPointUtils.isMoving(trackPoint)) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
isIdle = true;
lastTrackPoint = trackPoint;
return;
}
if (isIdle && TrackPointUtils.isMoving(trackPoint)) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
isIdle = false;
lastTrackPoint = trackPoint;
return;
}
Log.d(TAG, "Not recording TrackPoint, idle");
lastTrackPoint = trackPoint;
}
/**
* Inserts a trackPoint if this trackPoint is different than lastValidTrackPoint.
*
* @param track the track
* @param trackPoint the trackPoint
*/
private void insertTrackPointIfNewer(@NonNull Track track, @NonNull TrackPoint trackPoint) {
TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
if (TrackPointUtils.equalTime(trackPoint, lastValidTrackPoint)) {
// Do not insert if inserted already
Log.w(TAG, "Ignore insertTrackPoint. trackPoint time same as last valid track point time.");
return;
}
insertTrackPoint(track, trackPoint);
}
/**
* Inserts a trackPoint.
*
* @param track the track
* @param trackPoint the trackPoint
* @param lastValidTrackPoint the last valid track point, can be null
* @param track the track
* @param trackPoint the trackPoint
*/
private void insertTrackPoint(Track track, TrackPoint trackPoint, TrackPoint lastValidTrackPoint) {
if (trackPoint == null) {
Log.w(TAG, "Ignore insertLocation. trackPoint is null.");
return;
}
// Do not insert if inserted already
if (lastValidTrackPoint != null && lastValidTrackPoint.getTime() == trackPoint.getTime()) {
Log.w(TAG, "Ignore insertLocation. trackPoint time same as last valid track point time.");
return;
}
private void insertTrackPoint(@NonNull Track track, @NonNull TrackPoint trackPoint) {
try {
contentProviderUtils.insertTrackPoint(trackPoint, track.getId());
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
updateRecordingTrack(track);
updateTrackTotalTime(track);
} catch (SQLiteException e) {
/*
* Insert failed, most likely because of SqlLite error code 5 (SQLite_BUSY).
@@ -668,22 +692,31 @@ public class TrackRecordingService extends Service {
voiceExecutor.update();
}
/**
* Updates the recording track time.
*
* @param track the track
*/
private void updateRecordingTrack(Track track) {
private void updateTrackTotalTime(Track track) {
trackStatisticsUpdater.updateTime(System.currentTimeMillis());
track.setTrackStatistics(trackStatisticsUpdater.getTrackStatistics());
contentProviderUtils.updateTrack(track);
}
SensorDataSet getSensorDataSet() {
if (remoteSensorManager == null || !remoteSensorManager.isEnabled() || !remoteSensorManager.isSensorDataSetValid()) {
if (remoteSensorManager == null || !remoteSensorManager.isEnabled()) {
return null;
}
return remoteSensorManager.getSensorDataSet();
return remoteSensorManager.getSensorData();
}
private void fillWithSensorDataSet(TrackPoint trackPoint) {
SensorDataSet sensorData = getSensorDataSet();
if (sensorData != null) {
sensorData.fillTrackPoint(trackPoint);
}
}
private void registerLocationListener() {
@@ -736,4 +769,22 @@ public class TrackRecordingService extends Service {
notificationManager.cancelNotification();
}
}
/**
* 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;
}
}
@@ -5,6 +5,7 @@ import android.location.Location;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
/**
* TODO: There is a bug in Android that leaks Binder instances. This bug is
@@ -92,6 +93,18 @@ class TrackRecordingServiceBinder extends android.os.Binder implements TrackReco
return trackRecordingService.getSensorDataSet();
}
@VisibleForTesting
@Override
public void enableLocationExecutor(boolean enable) {
trackRecordingService.enableLocationExecutor(enable);
}
@VisibleForTesting
@Override
public void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager) {
trackRecordingService.setRemoteSensorManager(remoteSensorManager);
}
/**
* Detaches from the track recording service. Clears the reference to the
* outer class to minimize the leak.
@@ -20,6 +20,7 @@ import android.location.Location;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
/**
* App's service.
@@ -46,8 +47,6 @@ public interface TrackRecordingServiceInterface {
/**
* Resumes the track identified by trackId.
*
* @param trackId
*/
void resumeTrack(long trackId);
@@ -94,8 +93,13 @@ public interface TrackRecordingServiceInterface {
long insertWaypoint(String name, String category, String description, String photoUrl);
/**
* ONLY FOR TESTING
* Gets the current sensor data. Returns null if there is no data.
*
* @return SensorDataSet object.
*/
SensorDataSet getSensorData();
/**
* Inserts a location in the current recording track.
* <p>
* When recording a track, GPS locations are automatically inserted.
@@ -107,9 +111,11 @@ public interface TrackRecordingServiceInterface {
void insertLocation(Location location);
/**
* Gets the current sensor data. Returns null if there is no data.
*
* @return SensorDataSet object.
* Disables processing of location updates from {@link android.location.LocationManager}.
*/
SensorDataSet getSensorData();
@VisibleForTesting
void enableLocationExecutor(boolean enable);
@VisibleForTesting
void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager);
}
@@ -24,40 +24,30 @@ import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.UUID;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.content.sensor.SensorState;
import de.dennisguse.opentracks.content.sensor.SensorData;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
import de.dennisguse.opentracks.util.BluetoothUtils;
/**
* Manages connection to Bluetooth LE heart rate monitor.
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
* Also parses the transferred data into {@link SensorDataObserver}.
*/
public class BluetoothConnectionManager {
// Message types sent to handler
static final int MESSAGE_CONNECTING = 1;
static final int MESSAGE_CONNECTED = 2;
static final int MESSAGE_READ = 3;
static final int MESSAGE_DISCONNECTED = 4;
private static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
private static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
public abstract class BluetoothConnectionManager {
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
private final Context context;
private final Handler handler;
private SensorState sensorState;
private final SensorDataObserver observer;
private final UUID serviceUUUID;
private final UUID measurementUUID;
private BluetoothGatt bluetoothGatt;
private final BluetoothDevice bluetoothDevice;
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
@Override
@@ -65,86 +55,70 @@ public class BluetoothConnectionManager {
switch (newState) {
case BluetoothProfile.STATE_CONNECTING:
Log.d(TAG, "Connecting to sensor: " + gatt.getDevice());
setState(SensorState.CONNECTING);
handler.obtainMessage(MESSAGE_CONNECTING, gatt.getDevice().getName()).sendToTarget();
case BluetoothProfile.STATE_CONNECTED:
Log.d(TAG, "Connected to sensor: " + gatt.getDevice());
setState(SensorState.CONNECTED);
gatt.discoverServices();
handler.obtainMessage(MESSAGE_CONNECTED, gatt.getDevice().getName()).sendToTarget();
break;
case BluetoothProfile.STATE_DISCONNECTING:
Log.d(TAG, "Disconnecting from sensor: " + gatt.getDevice());
setState(SensorState.DISCONNECTING);
case BluetoothProfile.STATE_DISCONNECTED:
Log.d(TAG, "Disconnected from sensor: " + gatt.getDevice());
setState(SensorState.DISCONNECTED);
handler.obtainMessage(MESSAGE_DISCONNECTED, gatt.getDevice().getName()).sendToTarget();
}
}
@Override
public void onServicesDiscovered(@NonNull BluetoothGatt gatt, int status) {
BluetoothGattService service = gatt.getService(BluetoothUtils.HEART_RATE_SERVICE_UUID);
BluetoothGattService service = gatt.getService(serviceUUUID);
if (service == null) {
Log.e(TAG, "Could not get heart rate service for " + gatt.getDevice().getAddress());
Log.e(TAG, "Could not get service for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceUUUID);
return;
}
BluetoothGattCharacteristic characteristic = service.getCharacteristic(HEART_RATE_MEASUREMENT_CHAR_UUID);
BluetoothGattCharacteristic characteristic = service.getCharacteristic(measurementUUID);
if (characteristic == null) {
Log.e(TAG, "Could not get BluetoothCharacteristic for " + gatt.getDevice().getAddress());
Log.e(TAG, "Could not get BluetoothCharacteristic for address=" + gatt.getDevice().getAddress() + " serviceUUID=" + serviceUUUID + " characteristicUUID=" + measurementUUID);
return;
}
gatt.setCharacteristicNotification(characteristic, true);
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID);
// Register for updates.
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(BluetoothUtils.CLIENT_CHARACTERISTIC_CONFIG_UUID);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
int heartRate = BluetoothUtils.parseHeartRate(characteristic);
String deviceName = gatt.getDevice().getName();
String sensorName = gatt.getDevice().getName();
Log.d(TAG, "Received data from " + sensorName);
Log.d(TAG, "Received heart beat rate " + deviceName + ": " + heartRate);
SensorDataSet sensorDataSet = new SensorDataSet(heartRate, deviceName, gatt.getDevice().getAddress());
handler.obtainMessage(MESSAGE_READ, sensorDataSet).sendToTarget();
SensorData sensorData = parsePayload(sensorName, gatt.getDevice().getAddress(), characteristic);
if (sensorData != null) {
observer.onChanged(sensorData);
}
}
};
/**
* Constructor.
*
* @param handler a handler for sending messages back to the UI activity
*/
BluetoothConnectionManager(@NonNull Context context, @NonNull BluetoothDevice bluetoothDevice, @NonNull Handler handler) {
this.context = context;
this.bluetoothDevice = bluetoothDevice;
this.handler = handler;
this.sensorState = SensorState.NONE;
BluetoothConnectionManager(UUID serviceUUUID, UUID measurementUUID, SensorDataObserver observer) {
this.serviceUUUID = serviceUUUID;
this.measurementUUID = measurementUUID;
this.observer = observer;
}
public synchronized void connect() {
synchronized void connect(Context context, @NonNull BluetoothDevice device) {
if (bluetoothGatt != null) {
Log.w(TAG, "Already connected; ignoring.");
}
Log.d(TAG, "Connecting to: " + bluetoothDevice);
Log.d(TAG, "Connecting to: " + device);
bluetoothGatt = bluetoothDevice.connectGatt(this.context, true, this.connectCallback);
bluetoothGatt = device.connectGatt(context, true, this.connectCallback);
setState(SensorState.CONNECTING);
}
public synchronized void disconnect() {
synchronized void disconnect() {
if (bluetoothGatt == null) {
Log.w(TAG, "Cannot disconnect if not connected.");
return;
@@ -153,15 +127,78 @@ public class BluetoothConnectionManager {
bluetoothGatt = null;
}
public synchronized boolean isSameBluetoothDevice(String address) {
return this.bluetoothDevice.getAddress().equals(address);
synchronized boolean isSameBluetoothDevice(String address) {
if (bluetoothGatt == null) {
return false;
}
return address.equals(bluetoothGatt.getDevice().getAddress());
}
synchronized SensorState getSensorState() {
return sensorState;
/**
* @return null if data could not be parsed.
*/
protected abstract de.dennisguse.opentracks.content.sensor.SensorData parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic);
public static class HeartRate extends BluetoothConnectionManager {
HeartRate(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.HEART_RATE_SERVICE_UUID, BluetoothUtils.HEART_RATE_MEASUREMENT_CHAR_UUID, observer);
}
@Override
protected SensorDataHeartRate parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Integer heartRate = BluetoothUtils.parseHeartRate(characteristic);
return heartRate != null ? new SensorDataHeartRate(address, sensorName, heartRate) : null;
}
}
private synchronized void setState(SensorState sensorState) {
this.sensorState = sensorState;
public static class CyclingCadence extends BluetoothConnectionManager {
CyclingCadence(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
}
@Override
protected SensorDataCycling.Cadence parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (cadenceAndSpeed == null) {
return null;
}
if (cadenceAndSpeed.getCadence() != null) {
return cadenceAndSpeed.getCadence();
}
//Workaround for Wahoo CADENCE: this sensor reports speed (instead of cadence)
if (cadenceAndSpeed.getSpeed() != null) {
return new SensorDataCycling.Cadence(cadenceAndSpeed.getSpeed());
}
return null;
}
}
public static class CyclingSpeed extends BluetoothConnectionManager {
CyclingSpeed(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
}
@Override
protected SensorDataCycling.Speed parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (cadenceAndSpeed != null) {
return cadenceAndSpeed.getSpeed();
}
return null;
}
}
interface SensorDataObserver {
void onChanged(SensorData sensorData);
}
}
@@ -20,90 +20,71 @@ import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.widget.Toast;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.util.BluetoothUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* Bluetooth LE sensor manager.
* Should only be instantiated once!
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
*
* Note: should only be instantiated once.
*
* TODO: listen for Bluetooth enabled/disabled events.
*
* TODO: In case, a cycling (Cadence and Speed) sensor reports both values, testing is required.
* We establish two GATT separate GATT connections (as if two different sensors were used).
* However, it is not clear if this is allowed.
* Even if this works, it is not clear what happens if a user (while recording) changes one of the sensors in the settings as this will trigger a disconnect of one GATT.
*
* @author Sandor Dornbush
*/
public class BluetoothRemoteSensorManager {
public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.SensorDataObserver {
public static final long MAX_SENSOR_DATE_SET_AGE_MS = 5000;
private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName();
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
public static final long MAX_SENSOR_DATE_SET_AGE_MS = 5 * UnitConversions.S_TO_MS;
private static final BluetoothAdapter bluetoothAdapter = BluetoothUtils.getDefaultBluetoothAdapter(TAG);
private final Context context;
private final SharedPreferences sharedPreferences;
// Handler that gets information back from the bluetoothConnectionManager
private final Handler messageHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message message) {
String toastMessage;
switch (message.what) {
case BluetoothConnectionManager.MESSAGE_CONNECTING:
//Ignore for now.
toastMessage = context.getString(R.string.settings_sensor_connecting, message.obj);
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
break;
case BluetoothConnectionManager.MESSAGE_CONNECTED:
toastMessage = context.getString(R.string.settings_sensor_connected, message.obj);
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
break;
case BluetoothConnectionManager.MESSAGE_READ:
if (!(message.obj instanceof SensorDataSet)) {
Log.e(TAG, "Received message did not contain a SensorDataSet.");
sensorDataSet = null;
} else {
sensorDataSet = (SensorDataSet) message.obj;
}
break;
case BluetoothConnectionManager.MESSAGE_DISCONNECTED:
toastMessage = context.getString(R.string.settings_sensor_disconnected, message.obj);
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
break;
default:
Log.e(TAG, "Got an undefined case. Please check.");
break;
}
}
};
private boolean started = false;
private final BluetoothConnectionManager.HeartRate heartRate = new BluetoothConnectionManager.HeartRate(this);
private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
private final BluetoothConnectionManager.CyclingSpeed cyclingSpeed = new BluetoothConnectionManager.CyclingSpeed(this);
private final SensorDataSet sensorDataSet = new SensorDataSet();
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (bluetoothConnectionManager != null && PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_heart_rate_key, key)) {
if (PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(context)) {
stop();
return;
}
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
if (bluetoothConnectionManager.isSameBluetoothDevice(address)) {
return;
}
if (!started) return;
disconnect();
startCurrentSensor();
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_heart_rate_key, key)) {
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
connect(heartRate, address);
}
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_cadence_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingCadenceSensorAddress(context);
connect(cyclingCadence, address);
}
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_cadence_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingSpeedSensorAddress(context);
connect(cyclingSpeed, address);
}
}
};
private SensorDataSet sensorDataSet = null;
private BluetoothConnectionManager bluetoothConnectionManager;
/**
* @param context the context
*/
@@ -113,67 +94,76 @@ public class BluetoothRemoteSensorManager {
}
public void start() {
started = true;
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
startCurrentSensor();
//Trigger connection startup
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
}
public void stop() {
disconnect();
public synchronized void stop() {
// Disconnecting
heartRate.disconnect();
cyclingCadence.disconnect();
sensorDataSet.clear();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
started = false;
}
public boolean isEnabled() {
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
}
public SensorDataSet getSensorDataSet() {
return sensorDataSet;
}
public boolean isSensorDataSetValid() {
SensorDataSet sensorDataSet = getSensorDataSet();
if (sensorDataSet == null) {
return false;
}
return sensorDataSet.isRecent(MAX_SENSOR_DATE_SET_AGE_MS);
}
private void startCurrentSensor() {
private synchronized void connect(BluetoothConnectionManager connectionManager, String address) {
if (!isEnabled()) {
Log.w(TAG, "Bluetooth not enabled.");
return;
}
if (PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(context)) {
Log.w(TAG, "No bluetooth address.");
if (PreferencesUtils.isBluetoothSensorAddressNone(context, address)) {
Log.w(TAG, "No Bluetooth address.");
return;
}
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
// Check if there is an ongoing connection; if yes, check if the address changed.
if (connectionManager.isSameBluetoothDevice(address)) {
return;
} else {
connectionManager.disconnect();
}
Log.i(TAG, "Connecting to bluetooth address: " + address);
BluetoothDevice device;
try {
device = bluetoothAdapter.getRemoteDevice(address);
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
connectionManager.connect(context, device);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Unable to get remote device for: " + address, e);
String toastMessage = context.getString(R.string.sensor_not_known, address);
Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show();
return;
Log.e(TAG, "Unable to get remote device for: " + address, e);
}
disconnect();
bluetoothConnectionManager = new BluetoothConnectionManager(context, device, messageHandler);
bluetoothConnectionManager.connect();
}
private void disconnect() {
if (bluetoothConnectionManager != null) {
bluetoothConnectionManager.disconnect();
bluetoothConnectionManager = null;
public SensorDataSet getSensorData() {
return sensorDataSet;
}
@Override
public synchronized void onChanged(de.dennisguse.opentracks.content.sensor.SensorData sensorData) {
if (sensorData instanceof SensorDataCycling.Cadence) {
if (sensorData.equals(sensorDataSet.getCyclingCadence())) {
Log.d(TAG, "onChanged: cadence data repeated.");
return;
}
((SensorDataCycling.Cadence) sensorData).compute(sensorDataSet.getCyclingCadence());
}
if (sensorData instanceof SensorDataCycling.Speed) {
if (sensorData.equals(sensorDataSet.getCyclingSpeed())) {
Log.d(TAG, "onChanged: speed data repeated.");
return;
}
((SensorDataCycling.Speed) sensorData).compute(sensorDataSet.getCyclingSpeed(), PreferencesUtils.getWheelCircumference(context));
}
sensorDataSet.set(sensorData);
}
}
@@ -25,9 +25,12 @@ import android.util.Log;
import java.util.Locale;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.AnnouncementUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* This class will periodically announce the user's {@link TrackStatistics}.
@@ -47,6 +50,8 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
private final AudioManager audioManager;
private ContentProviderUtils contentProviderUtils;
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
@@ -103,6 +108,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
AnnouncementPeriodicTask(Context context) {
this.context = context;
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
contentProviderUtils = new ContentProviderUtils(context);
}
@Override
@@ -157,7 +163,9 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
Log.i(TAG, "Speech is not allowed at this time.");
return;
}
String announcement = AnnouncementUtils.getAnnouncement(context, trackStatistics);
Track track = contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context));
String category = track != null ? track.getCategory() : "";
String announcement = AnnouncementUtils.getAnnouncement(context, trackStatistics, category);
speakAnnouncement(announcement);
}
@@ -13,6 +13,9 @@ import androidx.preference.PreferenceFragmentCompat;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeSensorPreference;
import de.dennisguse.opentracks.util.BluetoothUtils;
import de.dennisguse.opentracks.util.HackUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -111,8 +114,10 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
} else if (preference instanceof ActivityTypePreference) {
activityPreferenceDialog = ActivityTypePreference.ActivityPreferenceDialog.newInstance(preference.getKey());
dialogFragment = activityPreferenceDialog;
} else if (preference instanceof BluetoothLePreference) {
dialogFragment = BluetoothLePreference.BluetoothLePreferenceDialog.newInstance(preference.getKey());
} else if (preference instanceof BluetoothLeHeartRatePreference) {
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.HEART_RATE_SERVICE_UUID);
} else if (preference instanceof BluetoothLeCyclingCadenceAndSpeedPreference) {
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID);
}
if (dialogFragment != null) {
@@ -139,7 +144,7 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
private void updateBluetooth() {
// Disable Bluetooth preference if device does not have Bluetooth
BluetoothLePreference bluetoothPreference = findPreference(getString(R.string.settings_sensor_bluetooth_heart_rate_key));
BluetoothLeSensorPreference bluetoothPreference = findPreference(getString(R.string.settings_sensor_bluetooth_heart_rate_key));
bluetoothPreference.setVisible(BluetoothUtils.hasBluetooth(TAG));
}
@@ -0,0 +1,23 @@
package de.dennisguse.opentracks.settings.bluetooth;
import android.content.Context;
import android.util.AttributeSet;
public class BluetoothLeCyclingCadenceAndSpeedPreference extends BluetoothLeSensorPreference {
public BluetoothLeCyclingCadenceAndSpeedPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public BluetoothLeCyclingCadenceAndSpeedPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public BluetoothLeCyclingCadenceAndSpeedPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BluetoothLeCyclingCadenceAndSpeedPreference(Context context) {
super(context);
}
}
@@ -0,0 +1,23 @@
package de.dennisguse.opentracks.settings.bluetooth;
import android.content.Context;
import android.util.AttributeSet;
public class BluetoothLeHeartRatePreference extends BluetoothLeSensorPreference {
public BluetoothLeHeartRatePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public BluetoothLeHeartRatePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public BluetoothLeHeartRatePreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BluetoothLeHeartRatePreference(Context context) {
super(context);
}
}
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.settings;
package de.dennisguse.opentracks.settings.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.le.BluetoothLeScanner;
@@ -21,8 +21,10 @@ import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.BluetoothLeAdapter;
import de.dennisguse.opentracks.util.BluetoothUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -30,25 +32,27 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
* Preference to select a discoverable Bluetooth LE device.
* Based upon ListPreference.
*/
public class BluetoothLePreference extends DialogPreference {
public abstract class BluetoothLeSensorPreference extends DialogPreference {
private static final String TAG = BluetoothLePreference.class.getSimpleName();
private static final String TAG = BluetoothLeSensorPreference.class.getSimpleName();
private static final String ARG_BLUETOOTH_UUID = "bluetoothUUID";
private static final int DEVICE_NONE_RESOURCEID = R.string.value_none;
public BluetoothLePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
public BluetoothLeSensorPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public BluetoothLePreference(Context context, AttributeSet attrs, int defStyleAttr) {
public BluetoothLeSensorPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public BluetoothLePreference(Context context, AttributeSet attrs) {
public BluetoothLeSensorPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BluetoothLePreference(Context context) {
public BluetoothLeSensorPreference(Context context) {
super(context);
}
@@ -78,14 +82,14 @@ public class BluetoothLePreference extends DialogPreference {
@Override
public CharSequence getSummary() {
if (getValue() == null || PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(getContext(), getValue())) {
if (getValue() == null || PreferencesUtils.isBluetoothSensorAddressNone(getContext(), getValue())) {
return getContext().getString(DEVICE_NONE_RESOURCEID);
}
return getValue();
}
public static class BluetoothLePreferenceDialog extends PreferenceDialogFragmentCompat {
public static class BluetoothLeSensorPreferenceDialog extends PreferenceDialogFragmentCompat {
private int selectedEntryIndex;
private final BluetoothLeAdapter listAdapter = new BluetoothLeAdapter();
@@ -116,10 +120,11 @@ public class BluetoothLePreference extends DialogPreference {
}
};
static BluetoothLePreferenceDialog newInstance(String key) {
final BluetoothLePreferenceDialog fragment = new BluetoothLePreferenceDialog();
public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, UUID sensorUUID) {
final BluetoothLeSensorPreferenceDialog fragment = new BluetoothLeSensorPreferenceDialog();
final Bundle b = new Bundle(1);
b.putString(ARG_KEY, key);
b.putString(ARG_KEY, preferenceKey);
b.putSerializable(ARG_BLUETOOTH_UUID, sensorUUID);
fragment.setArguments(b);
return fragment;
}
@@ -128,6 +133,9 @@ public class BluetoothLePreference extends DialogPreference {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UUID sensorUUID = (UUID) getArguments().getSerializable(ARG_BLUETOOTH_UUID);
Log.i(TAG, "UUID: " + sensorUUID);
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getDefaultBluetoothAdapter(TAG);
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
Log.w(TAG, "Bluetooth adapter is present or not enabled.");
@@ -137,7 +145,7 @@ public class BluetoothLePreference extends DialogPreference {
}
if (bluetoothAdapter.isDiscovering()) {
Log.i(TAG, "Cancelling ongoing bluetooth discovery.");
Log.i(TAG, "Cancelling ongoing B<aluetooth discovery.");
bluetoothAdapter.cancelDiscovery();
}
@@ -152,14 +160,14 @@ public class BluetoothLePreference extends DialogPreference {
listAdapter.add(getContext().getString(DEVICE_NONE_RESOURCEID), deviceNone);
selectedEntryIndex = 0;
BluetoothLePreference preference = (BluetoothLePreference) getPreference();
BluetoothLeSensorPreference preference = (BluetoothLeSensorPreference) getPreference();
String deviceSelected = preference.getValue();
if (deviceSelected != null && !deviceNone.equals(deviceSelected)) {
listAdapter.add(preference.getValue(), preference.getValue());
selectedEntryIndex = 1;
}
ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder().setServiceUuid(new ParcelUuid(BluetoothUtils.HEART_RATE_SERVICE_UUID));
ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder().setServiceUuid(new ParcelUuid(sensorUUID));
List<ScanFilter> scanFilter = new ArrayList<>();
scanFilter.add(scanFilterBuilder.build());
@@ -180,7 +188,7 @@ public class BluetoothLePreference extends DialogPreference {
public void onClick(DialogInterface dialog, int which) {
selectedEntryIndex = which;
BluetoothLePreferenceDialog.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
BluetoothLeSensorPreferenceDialog.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
dialog.dismiss();
}
});
@@ -196,7 +204,7 @@ public class BluetoothLePreference extends DialogPreference {
if (positiveResult && selectedEntryIndex >= 0) {
String value = listAdapter.get(selectedEntryIndex).getAddress();
BluetoothLePreference preference = (BluetoothLePreference) getPreference();
BluetoothLeSensorPreference preference = (BluetoothLeSensorPreference) getPreference();
if (preference.callChangeListener(value)) {
preference.setValue(value);
}
@@ -176,6 +176,10 @@ public class TrackStatistics {
this.maxSpeed_mps = maxSpeed;
}
public boolean hasElevationMin() {
return !Double.isInfinite(getMinElevation());
}
/**
* Gets the minimum elevation.
* This is calculated from the smoothed elevation, so this can actually be more than the current elevation.
@@ -193,6 +197,10 @@ public class TrackStatistics {
elevationExtremities.setMin(elevation);
}
public boolean hasElevationMax() {
return !Double.isInfinite(getMaxElevation());
}
/**
* Gets the maximum elevation.
* This is calculated from the smoothed elevation, so this can actually be less than the current elevation.
@@ -18,19 +18,20 @@ package de.dennisguse.opentracks.stats;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.util.LocationUtils;
import static de.dennisguse.opentracks.services.TrackRecordingService.MAX_NO_MOVEMENT_SPEED;
import de.dennisguse.opentracks.util.TrackPointUtils;
/**
* Updater for {@link TrackStatistics}.
* For updating track {@link TrackStatistics} as new {@link TrackPoint}s are added.
* NOTE: Some of the locations represent pause/resume separator.
* NOTE: Has still support for segments (at the moment unused).
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
@@ -116,16 +117,16 @@ public class TrackStatisticsUpdater {
*/
public void addTrackPoint(TrackPoint trackPoint, int minRecordingDistance) {
// Always update time
updateTime(trackPoint.getLocation().getTime());
updateTime(trackPoint.getTime());
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
// Either pause or resume marker
if (trackPoint.getLocation().getLatitude() == TrackPointsColumns.PAUSE_LATITUDE) {
if (trackPoint.getLatitude() == TrackPointsColumns.PAUSE_LATITUDE) {
if (lastTrackPoint != null && lastMovingTrackPoint != null && lastTrackPoint != lastMovingTrackPoint) {
currentSegment.addTotalDistance(lastMovingTrackPoint.distanceTo(lastTrackPoint));
}
trackStatistics.merge(currentSegment);
}
currentSegment = init(trackPoint.getLocation().getTime());
currentSegment = init(trackPoint.getTime());
lastTrackPoint = null;
lastMovingTrackPoint = null;
elevationBuffer_m.reset();
@@ -143,7 +144,7 @@ public class TrackStatisticsUpdater {
}
double movingDistance = lastMovingTrackPoint.distanceTo(trackPoint);
if (movingDistance < minRecordingDistance && (!trackPoint.hasSpeed() || trackPoint.getSpeed() < MAX_NO_MOVEMENT_SPEED)) {
if (movingDistance < minRecordingDistance && !TrackPointUtils.isMoving(trackPoint)) {
speedBuffer_ms.reset();
lastTrackPoint = trackPoint;
return;
@@ -162,7 +163,7 @@ public class TrackStatisticsUpdater {
// Update max speed
if (trackPoint.hasSpeed() && lastTrackPoint.hasSpeed()) {
updateSpeed(trackPoint.getTime(), trackPoint.getSpeed(), lastTrackPoint.getTime(), lastTrackPoint.getSpeed());
updateSpeed(trackPoint, lastTrackPoint);
}
lastTrackPoint = trackPoint;
@@ -190,23 +191,18 @@ public class TrackStatisticsUpdater {
/**
* Updates a speed reading while assuming the user is moving.
*
* @param time the time
* @param speed the speed
* @param lastLocationTime the last location time
* @param lastLocationSpeed the last location speed
*/
@VisibleForTesting
private void updateSpeed(long time, double speed, long lastLocationTime, double lastLocationSpeed) {
if (speed < MAX_NO_MOVEMENT_SPEED) {
private void updateSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
if (!TrackPointUtils.isMoving(trackPoint)) {
speedBuffer_ms.reset();
} else if (isValidSpeed(time, speed, lastLocationTime, lastLocationSpeed)) {
speedBuffer_ms.setNext(speed);
} else if (isValidSpeed(trackPoint, lastTrackPoint)) {
speedBuffer_ms.setNext(trackPoint.getSpeed());
if (speedBuffer_ms.getAverage() > currentSegment.getMaxSpeed()) {
currentSegment.setMaxSpeed(speedBuffer_ms.getAverage());
}
} else {
Log.d(TAG, "Invalid speed. speed: " + speed + " lastLocationSpeed: " + lastLocationSpeed);
Log.d(TAG, "Invalid speed. speed: " + trackPoint.getSpeed() + " lastLocationSpeed: " + lastTrackPoint.getSpeed());
}
}
@@ -239,26 +235,21 @@ public class TrackStatisticsUpdater {
/**
* Returns true if the speed is valid.
*
* @param time the time
* @param speed the speed
* @param lastLocationTime the last location time
* @param lastLocationSpeed the last location speed
*/
private boolean isValidSpeed(long time, double speed, long lastLocationTime, double lastLocationSpeed) {
private boolean isValidSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
// There are a lot of noisy speed readings. Do the cheapest checks first, most expensive last.
if (speed == 0) {
if (trackPoint.getSpeed() == 0) {
return false;
}
// The following code will ignore unlikely readings. 128 m/s seems to be an internal android error code.
if (Math.abs(speed - 128) < 1) {
if (Math.abs(trackPoint.getSpeed() - 128) < 1) {
return false;
}
// See if the speed seems physically likely. Ignore any speeds that imply acceleration greater than 2g.
long timeDifference = time - lastLocationTime;
double speedDifference = Math.abs(lastLocationSpeed - speed);
long timeDifference = trackPoint.getTime() - lastTrackPoint.getTime();
double speedDifference = Math.abs(lastTrackPoint.getSpeed() - trackPoint.getSpeed());
if (speedDifference > MAX_ACCELERATION * timeDifference) {
return false;
}
@@ -266,8 +257,8 @@ public class TrackStatisticsUpdater {
// Only check if the speed buffer is full. Check that the speed is less than 10X the smoothed average and the speed difference doesn't imply 2g acceleration.
if (speedBuffer_ms.isFull()) {
double average = speedBuffer_ms.getAverage();
double diff = Math.abs(average - speed);
return (speed < average * 10) && (diff < MAX_ACCELERATION * timeDifference);
double diff = Math.abs(average - trackPoint.getSpeed());
return (trackPoint.getSpeed() < average * 10) && (diff < MAX_ACCELERATION * timeDifference);
}
return true;
@@ -9,9 +9,9 @@ public class AnnouncementUtils {
private AnnouncementUtils() {}
public static String getAnnouncement(Context context, TrackStatistics trackStatistics) {
public static String getAnnouncement(Context context, TrackStatistics trackStatistics, String category) {
boolean metricUnits = PreferencesUtils.isMetricUnits(context);
boolean reportSpeed = PreferencesUtils.isReportSpeed(context);
boolean reportSpeed = PreferencesUtils.isReportSpeed(context, category);
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
double distancePerTime = trackStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
@@ -21,9 +21,13 @@ import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.UUID;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
/**
* Utilities for dealing with bluetooth devices.
*
@@ -31,11 +35,21 @@ import java.util.UUID;
*/
public class BluetoothUtils {
public static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
public static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
public static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
public static final UUID CYCLING_SPEED_CADENCE_SERVICE_UUID = new UUID(0x181600001000L, 0x800000805f9b34fbL);
public static final UUID CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID = new UUID(0x2A5B00001000L, 0x800000805f9b34fbL);
private BluetoothUtils() {
}
/**
* If called from UI: use a background thread to get the default Bluetooth adapter.
* TODO Check if this is necessary.
*/
public static BluetoothAdapter getDefaultBluetoothAdapter(final String TAG) {
// If from the main application thread, return directly
if (Thread.currentThread().equals(Looper.getMainLooper().getThread())) {
@@ -77,12 +91,56 @@ public class BluetoothUtils {
return BluetoothUtils.getDefaultBluetoothAdapter(TAG) != null;
}
public static int parseHeartRate(BluetoothGattCharacteristic characteristic) {
public static Integer parseHeartRate(BluetoothGattCharacteristic characteristic) {
//DOCUMENTATION https://www.bluetooth.com/specifications/gatt/characteristics/
byte[] raw = characteristic.getValue();
int index = ((raw[0] & 0x1) == 1) ? 2 : 1;
int format = (index == 1) ? BluetoothGattCharacteristic.FORMAT_UINT8 : BluetoothGattCharacteristic.FORMAT_UINT16;
return characteristic.getIntValue(format, index);
if (raw.length == 0) {
return null;
}
boolean formatUINT16 = ((raw[0] & 0x1) == 1);
if (formatUINT16 && raw.length >= 3) {
return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 1);
}
if (!formatUINT16 && raw.length >= 2) {
return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 1);
}
return null;
}
/**
* Documentation: https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=261449
*/
public static SensorDataCycling.CadenceAndSpeed parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
int valueLength = characteristic.getValue().length;
if (valueLength == 0) {
return null;
}
int flags = characteristic.getValue()[0];
boolean hasCrank = (flags & 0x01) > 0;
boolean hasWheel = (flags & 0x02) > 0;
SensorDataCycling.Cadence cadence = null;
int index = 1;
if (hasCrank && valueLength - index >= 6) {
long crankCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, index);
index += 4;
int crankTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
index += 2;
cadence = new SensorDataCycling.Cadence(address, sensorName, crankCount, crankTime);
}
SensorDataCycling.Speed speed = null;
if (hasWheel && valueLength - index >= 4) {
int wheelCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
index += 2;
int wheelTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
speed = new SensorDataCycling.Speed(address, sensorName, wheelCount, wheelTime);
}
return new SensorDataCycling.CadenceAndSpeed(address, sensorName, cadence, speed);
}
}
@@ -85,7 +85,7 @@ public class DialogUtils {
* @param formatArgs the format arguments for the message id
*/
private static ProgressDialog createProgressDialog(boolean spinner, final Context context, int messageId, DialogInterface.OnCancelListener onCancelListener, Object... formatArgs) {
final ProgressDialog progressDialog = new ProgressDialog(context);
final ProgressDialog progressDialog = new ProgressDialog(context, R.style.ThemeCustomNotTransparent);
progressDialog.setCancelable(true);
progressDialog.setCanceledOnTouchOutside(false);
progressDialog.setIcon(R.drawable.ic_dialog_info_24dp);
@@ -95,11 +95,15 @@ public class FileUtils {
/**
* Gets the extension from a file name.
* Returns null if there is no extension.
*
* @param fileName the file name
* @return null if there is no extension or fileName is null.
*/
public static String getExtension(String fileName) {
if (fileName == null) {
return null;
}
int index = fileName.lastIndexOf('.');
if (index == -1) {
return null;
@@ -250,4 +254,50 @@ public class FileUtils {
public static Uri getUriForFile(Context context, File file) {
return FileProvider.getUriForFile(context, FileUtils.FILEPROVIDER, file);
}
/**
* Checks that there is a file inside track photo directory whose name is the same that uri file.
* If there is a file inside photo directory whose name is the same that uri then returns File. Otherwise returns null.
*
* @param context the Context.
* @param trackId the id of the Track.
* @param uri the uri to check.
* @return File object or null.
*/
public static File getPhotoFileIfExists(Context context, long trackId, Uri uri) {
if (uri == null) {
Log.w(TAG, "URI object is null.");
return null;
}
String filename = uri.getLastPathSegment();
if (filename == null) {
Log.w(TAG, "External photo contains no filename.");
return null;
}
File dir = FileUtils.getPhotoDir(context, trackId);
File file = new File(dir, filename);
if (!file.exists()) {
return null;
}
return file;
}
/**
* Delete the directory recursively.
*
* @param file the directory
*/
public static void deleteDirectoryRecurse(File file) {
if (file != null && file.exists() && file.isDirectory()) {
for (File child : file.listFiles()) {
deleteDirectoryRecurse(child);
}
file.delete();
} else if (file != null && file.isFile()) {
file.delete();
}
}
}
@@ -17,9 +17,9 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
*/
public class IntentDashboardUtils {
public static final String ACTION_DASHBOARD = "Intent.OpenTracks-Dashboard";
private static final String ACTION_DASHBOARD = "Intent.OpenTracks-Dashboard";
public static final String ACTION_DASHBOARD_PAYLOAD = ACTION_DASHBOARD + ".Payload";
private static final String ACTION_DASHBOARD_PAYLOAD = ACTION_DASHBOARD + ".Payload";
private IntentDashboardUtils() {
}
@@ -22,7 +22,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
/**
@@ -90,13 +89,13 @@ public class LocationUtils {
*
* @param tolerance in meters
* @param trackPoints input
* @param decimated output
*/
//TODO What was it used for? Sharing data with other apps?
private static void decimate(double tolerance, List<TrackPoint> trackPoints, List<TrackPoint> decimated) {
private static List<TrackPoint> decimate(double tolerance, List<TrackPoint> trackPoints) {
List<TrackPoint> decimated = new ArrayList<>();
final int n = trackPoints.size();
if (n < 1) {
return;
return null;
}
int idx;
int maxIdx = 0;
@@ -142,18 +141,8 @@ public class LocationUtils {
idx++;
}
Log.d(TAG, "Decimating " + n + " points to " + i + " w/ tolerance = " + tolerance);
}
/**
* Decimates the given track for the given precision.
*
* @param track a track
* @param precision desired precision in meters
*/
public static void decimate(Track track, double precision) {
ArrayList<TrackPoint> decimated = new ArrayList<>();
decimate(precision, track.getTrackPoints(), decimated);
track.setTrackPoints(decimated);
return decimated;
}
/**
@@ -49,7 +49,7 @@ public class PreferencesUtils {
@Deprecated
//NOTE: is at the moment still used to determine if a track is currently recorded; better ask the service directly.
//NOTE: This was also used to recover from a reboot, but this data should not be exposed to the whole application.
//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;
public static long getRecordingTrackId(Context context) {
@@ -197,9 +197,14 @@ public class PreferencesUtils {
return STATS_UNIT.equals(getString(context, R.string.stats_units_key, STATS_UNIT));
}
public static boolean isReportSpeed(Context context) {
public static boolean isReportSpeed(Context context, String category) {
final String STATS_RATE_DEFAULT = context.getString(R.string.stats_rate_default);
return STATS_RATE_DEFAULT.equals(getString(context, R.string.stats_rate_key, STATS_RATE_DEFAULT));
String currentStatsRate = getString(context, R.string.stats_rate_key, STATS_RATE_DEFAULT);
if (currentStatsRate.equals(getString(context, R.string.stats_rate_speed_or_pace_default, STATS_RATE_DEFAULT))) {
return TrackIconUtils.isSpeedIcon(context, category);
}
return currentStatsRate.equals(context.getString(R.string.stats_rate_speed));
}
public static boolean isRecordingTrackPaused(Context context) {
@@ -215,20 +220,44 @@ public class PreferencesUtils {
setBoolean(context, R.string.recording_track_paused_key, RECORDING_TRACK_PAUSED);
}
private static String getBluetoothHeartRateSensorAddressNone(Context context) {
private static String getBluetoothSensorAddressNone(Context context) {
return context.getString(R.string.sensor_type_value_none);
}
public static boolean isBluetoothHeartRateSensorAddressDefault(Context context) {
return getBluetoothHeartRateSensorAddressNone(context).equals(getBluetoothHeartRateSensorAddress(context));
public static boolean isBluetoothSensorAddressNone(Context context, String currentValue) {
return getBluetoothSensorAddressNone(context).equals(currentValue);
}
public static boolean isBluetoothHeartRateSensorAddressDefault(Context context, String currentValue) {
return getBluetoothHeartRateSensorAddressNone(context).equals(currentValue);
public static boolean isBluetoothHeartRateSensorAddressNone(Context context) {
return isBluetoothSensorAddressNone(context, getBluetoothHeartRateSensorAddress(context));
}
public static String getBluetoothHeartRateSensorAddress(Context context) {
return getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothHeartRateSensorAddressNone(context));
return getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothSensorAddressNone(context));
}
public static boolean isBluetoothCyclingCadenceSensorAddressNone(Context context) {
return isBluetoothSensorAddressNone(context, getBluetoothCyclingCadenceSensorAddress(context));
}
public static String getBluetoothCyclingCadenceSensorAddress(Context context) {
return getString(context, R.string.settings_sensor_bluetooth_cycling_cadence_key, getBluetoothSensorAddressNone(context));
}
public static boolean isBluetoothCyclingSpeedSensorAddressNone(Context context) {
return isBluetoothSensorAddressNone(context, getBluetoothCyclingSpeedSensorAddress(context));
}
public static String getBluetoothCyclingSpeedSensorAddress(Context context) {
return getString(context, R.string.settings_sensor_bluetooth_cycling_speed_key, getBluetoothSensorAddressNone(context));
}
public static int getWheelCircumference(Context context) {
final int DEFAULT = Integer.parseInt(context.getResources().getString(R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_default));
return getInt(context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, DEFAULT);
}
public static boolean shouldShowStatsOnLockscreen(Context context) {
@@ -59,6 +59,21 @@ public class TrackIconUtils {
private static final int[] SNOW_BOARDING_LIST = new int[]{R.string.activity_type_snow_boarding};
private static final int[] WALK_LIST = new int[]{R.string.activity_type_hiking, R.string.activity_type_off_trail_hiking, R.string.activity_type_speed_walking, R.string.activity_type_trail_hiking, R.string.activity_type_walking};
// List of icons whose sports associated use speed (in km/h or mi/h).
private static final int[] SPEED_ICON = {
// Unknown.
R.string.activity_type_unknown,
// All airplane categories.
R.string.activity_type_airplane, R.string.activity_type_commercial_airplane, R.string.activity_type_rc_airplane,
// All bike categories.
R.string.activity_type_biking, R.string.activity_type_cycling, R.string.activity_type_dirt_bike, R.string.activity_type_motor_bike, R.string.activity_type_mountain_biking, R.string.activity_type_road_biking, R.string.activity_type_track_cycling,
// All boat categories.
R.string.activity_type_boat, R.string.activity_type_ferry, R.string.activity_type_motor_boating, R.string.activity_type_rc_boat,
// All drive categories.
R.string.activity_type_atv, R.string.activity_type_driving, R.string.activity_type_driving_bus, R.string.activity_type_driving_car,
};
private static final LinkedHashMap<String, Pair<Integer, Integer>> MAP = new LinkedHashMap<>();
static {
@@ -192,4 +207,14 @@ public class TrackIconUtils {
}
return false;
}
/**
* Returns true if category is in the SPEED_ICON array. Otherwise returns false.
*
* @param context the context.
* @param category the name of the category, activity type.
*/
public static boolean isSpeedIcon(Context context, String category) {
return inList(context, category, SPEED_ICON);
}
}
@@ -0,0 +1,66 @@
package de.dennisguse.opentracks.util;
import android.util.Log;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.data.TrackPoint;
public class TrackPointUtils {
// Anything faster than that (in meters per second) will be considered moving.
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
private final static String TAG = TrackPointUtils.class.getSimpleName();
private TrackPointUtils() {
}
/**
* 1. Ancient fix for phones that do not set the time in {@link android.location.Location}.
* 2. Fix for GPS time rollover happening every 19.7 years: https://en.wikipedia.org/wiki/GPS_Week_Number_Rollover
*/
public static void fixTime(@NonNull TrackPoint trackPoint) {
if (trackPoint.getTime() == 0L) {
Log.w(TAG, "Time of provided location was 0. Using current time.");
trackPoint.setTime(System.currentTimeMillis());
return;
}
{
long timeDiff = Math.abs(trackPoint.getTime() - System.currentTimeMillis());
if (timeDiff > 1023 * UnitConversions.ONE_WEEK_MS) {
Log.w(TAG, "GPS week rollover.");
trackPoint.setTime(trackPoint.getTime() + 1024 * UnitConversions.ONE_WEEK_MS);
}
}
}
public static boolean isMoving(@NonNull TrackPoint trackPoint) {
return trackPoint.hasSpeed() && trackPoint.getSpeed() >= MAX_NO_MOVEMENT_SPEED;
}
public static boolean equalTime(TrackPoint t1, TrackPoint t2) {
if (t1 == null || t2 == null) {
return false;
}
return t1.getTime() == t2.getTime();
}
public static boolean after(TrackPoint t1, TrackPoint t2) {
if (t1 == null || t2 == null) {
return false;
}
return t1.getTime() > t2.getTime();
}
/**
* Is accuracy better than threshold?
*/
public static boolean fulfillsAccuracy(@NonNull TrackPoint trackPoint, int poorAccuracy) {
return trackPoint.hasAccuracy() && trackPoint.getAccuracy() < poorAccuracy;
}
}
@@ -0,0 +1,30 @@
package de.dennisguse.opentracks.util;
public class UintUtils {
public static int UINT16_MAX = 0xFFFF;
public static long UINT32_MAX = 0xFFFFFFFFL;
private UintUtils() {
}
/**
* Computes a - b for UINT with overflow (b < a).
*
* @return diff or -1 (invalid)
*/
public static long diff(long a, long b, final long UINT_MAX) {
if (a < 0 || b < 0) {
return -1;
}
if (a > UINT_MAX && b > UINT_MAX) {
return -1;
}
if (a >= b) {
return a - b;
}
return (UINT_MAX - b) + a;
}
}
@@ -29,6 +29,9 @@ public class UnitConversions {
// 1 second in milliseconds
public static final long ONE_SECOND_MS = UnitConversions.S_TO_MS;
public static final long ONE_MINUTE_MS = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
public static final long ONE_HOUR_MS = 60 * ONE_MINUTE_MS;
public static final long ONE_DAY_MS = 24 * ONE_HOUR_MS;
public static final long ONE_WEEK_MS = 7 * ONE_DAY_MS;
// multiplication factor to convert milliseconds to seconds
public static final double MS_TO_S = 1d / S_TO_MS;
@@ -43,6 +46,8 @@ public class UnitConversions {
// multiplication factor to convert kilometers to miles
public static final double KM_TO_MI = 0.621371192;
public static final double MM_TO_M = 0.001;
// Distance
// multiplication factor to convert miles to feet
public static final double MI_TO_FT = 5280.0;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 B

-6
View File
@@ -1,6 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/>
<path android:fillColor="#FF000000" android:pathData="M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z"/>
</vector>
-30
View File
@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Non focused states -->
<item android:drawable="@drawable/tab_unselected" android:state_focused="false" android:state_pressed="false" android:state_selected="false" />
<item android:drawable="@drawable/tab_selected" android:state_focused="false" android:state_pressed="false" android:state_selected="true" />
<!-- Focused states -->
<item android:drawable="@drawable/tab_unselected_focused" android:state_focused="true" android:state_pressed="false" android:state_selected="false" />
<item android:drawable="@drawable/tab_selected_focused" android:state_focused="true" android:state_pressed="false" android:state_selected="true" />
<!-- Pressed -->
<!-- Non focused states -->
<item android:drawable="@drawable/tab_unselected_pressed" android:state_focused="false" android:state_pressed="true" android:state_selected="false" />
<item android:drawable="@drawable/tab_selected_pressed" android:state_focused="false" android:state_pressed="true" android:state_selected="true" />
<!-- Focused states -->
<item android:drawable="@drawable/tab_unselected_pressed" android:state_focused="true" android:state_pressed="true" android:state_selected="false" />
<item android:drawable="@drawable/tab_selected_pressed" android:state_focused="true" android:state_pressed="true" android:state_selected="true" />
</selector>
+30 -8
View File
@@ -267,18 +267,24 @@ limitations under the License.
android:id="@+id/stats_sensor_separator"
style="@style/StatsHorizontalLine" />
<!-- Sensor data: heart rate-->
<!-- Sensor data -->
<LinearLayout
android:id="@+id/stats_sensor_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout style="@style/StatsLargeItemContainer">
<LinearLayout
android:id="@+id/stats_sensor_heart_rate_container"
style="@style/StatsLargeItemContainer">
<TextView
style="@style/StatsLargeLabel"
android:text="@string/sensor_state_heart_rate" />
<TextView
android:id="@+id/stats_sensor_heart_rate_sensor_value"
style="@style/StatsTinyValue"
android:text="@string/value_unknown" />
<LinearLayout style="@style/StatsLargeValueContainer">
<TextView
@@ -291,20 +297,36 @@ limitations under the License.
android:text="@string/sensor_unit_beats_per_minute" />
</LinearLayout>
</LinearLayout>
<LinearLayout style="@style/StatsSmallGroupContainer">
<LinearLayout
android:id="@+id/stats_sensor_cadence_container"
style="@style/StatsLargeItemContainer">
<TextView
style="@style/StatsSmallLabel"
android:text="@string/settings_sensor_bluetooth_sensor" />
style="@style/StatsLargeLabel"
android:text="@string/sensor_state_cadence" />
<TextView
android:id="@+id/stats_sensor_heart_rate_sensor_value"
android:id="@+id/stats_sensor_cadence_sensor_value"
style="@style/StatsTinyValue"
android:value="@string/value_unknown" />
android:text="@string/value_unknown" />
<LinearLayout style="@style/StatsLargeValueContainer">
<TextView
android:id="@+id/stats_sensor_cadence_value"
style="@style/StatsLargeValue"
android:value="@string/value_unknown" />
<TextView
style="@style/StatsUnit"
android:text="@string/sensor_unit_rounds_per_minute" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
+2
View File
@@ -21,10 +21,12 @@ limitations under the License.
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom" />
<!-- Icon and title is overwritten by TrackListActivity -->
<!-- Hide icon as workaround for https://github.com/OpenTracksApp/OpenTracks/issues/200 -->
<item
android:id="@+id/track_list_start_gps"
android:icon="@drawable/ic_gps_off_24dp"
android:title="@string/menu_start_gps"
android:visible="false"
app:showAsAction="ifRoom" />
<item
android:id="@+id/track_list_markers"

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