Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23175902fb | |||
| 706bd8b01f | |||
| dc4ac07e11 | |||
| 5be4129ba9 | |||
| f75a48ccab | |||
| 2815aa4b62 | |||
| 0383b0e1c7 | |||
| 1601741495 | |||
| be1fae956b | |||
| c7557d80bb | |||
| ac28124664 | |||
| 1deaf7650e | |||
| 9074fa1b6c | |||
| 5721a615a2 | |||
| fadcf1cd41 | |||
| a50b1ac78e | |||
| 9a7d4fdb36 | |||
| 9fb51a689d | |||
| 65bda35001 | |||
| 443ded86a5 | |||
| 83b5ebb02c | |||
| 240d8f6b6c | |||
| 39c5422341 | |||
| 4893a2b743 | |||
| 3408ea97f4 | |||
| ddb17f61cb | |||
| bbe182cf82 | |||
| b7b7bdf19b | |||
| 5681938a9e | |||
| 74835dcef5 | |||
| ed0dc05366 | |||
| 653eb8320a | |||
| 54b9c4370b | |||
| fa9cb3903c | |||
| 0f1d8500d1 | |||
| 20d7fd2c03 | |||
| 1d838344dc | |||
| fd826199e2 | |||
| f485d90ef2 | |||
| ec39e67a09 | |||
| 1a86db0a8b | |||
| 6a1fada22f | |||
| 2898e06246 | |||
| 0d87c65b49 | |||
| 9b97cdfcd1 | |||
| 95feae846a | |||
| b09eeaede3 | |||
| 35dec1634c | |||
| e49a425106 | |||
| adbad04867 | |||
| 850bd2f887 |
@@ -6,7 +6,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
api-level: [21, 29]
|
||||
api-level: [29]
|
||||
target: [default]
|
||||
steps:
|
||||
- name: checkout
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
Before Width: | Height: | Size: 100 B |
|
Before Width: | Height: | Size: 105 B |
|
Before Width: | Height: | Size: 105 B |
|
Before Width: | Height: | Size: 102 B |
|
Before Width: | Height: | Size: 111 B |
|
Before Width: | Height: | Size: 111 B |
|
Before Width: | Height: | Size: 96 B |
|
Before Width: | Height: | Size: 102 B |
|
Before Width: | Height: | Size: 102 B |
|
Before Width: | Height: | Size: 103 B |
|
Before Width: | Height: | Size: 107 B |
|
Before Width: | Height: | Size: 110 B |
|
Before Width: | Height: | Size: 104 B |
|
Before Width: | Height: | Size: 110 B |
|
Before Width: | Height: | Size: 110 B |
|
Before Width: | Height: | Size: 114 B |
|
Before Width: | Height: | Size: 122 B |
|
Before Width: | Height: | Size: 121 B |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||