Compare commits

...

73 Commits

Author SHA1 Message Date
Dennis Guse 928781391d Release v3.4.0 2020-02-05 23:41:55 +01:00
Dennis Guse 05cd672464 Updated help text. 2020-02-05 23:41:50 +01:00
Dennis Guse d3109bbc37 Cleanup. 2020-02-05 23:08:17 +01:00
Dennis Guse eabdb7c721 Unify order of show on map and share menu entries. 2020-02-04 19:52:05 +01:00
Peter Storch ad990e8cd7 Forward multiple tracks to dashboard 2020-02-04 00:07:17 +01:00
Dennis Guse db3eef0104 Test: date comparison tends to be flaky. 2020-02-03 23:27:19 +01:00
Dennis Guse c4f1ba7962 Upgrade material design dependency.
Requires including androidx.documentfile directly.
2020-02-03 23:27:19 +01:00
Dennis Guse 7451fbd413 CI: enable testing via Github Actions. 2020-02-03 23:27:19 +01:00
Dennis Guse b6c55de55a CI: enable testing TravisCI on again. 2020-02-03 23:27:19 +01:00
Dennis Guse dd3ee6d99e Bugfix: match internal ContentURI correctly.
TrackPoint by TrackId was not working (and not used).
2020-02-03 21:32:06 +01:00
Dennis Guse bbb6a7c22c Bugfix: keep TrackRecordingService in foreground if paused. 2020-02-03 20:45:14 +01:00
Dennis Guse dc1b50a7bf Cleanup: move PAUSE_LATITUDE and RESUME_LATITUDE to TrackPointsColumn. 2020-02-03 20:45:14 +01:00
Dennis Guse 21bfb88e0b Bugfix: initialize fragments from bundle rather via constructor.
Introduced in 8b11e1d611
This fixes #127.
2020-02-03 19:51:04 +01:00
Dennis Guse aaab8adce9 ExportAsyncTask: always get a wakelock. 2020-02-03 19:51:04 +01:00
Dennis Guse df502f835c Release v3.3.2 2020-01-11 20:33:47 +01:00
Dennis Guse dd6a4cd153 Export: files are now overwritten. 2020-01-11 20:29:20 +01:00
Dennis Guse 76656997ef Sharing: check if a compatible app is installed rather than trying it. 2020-01-11 20:27:43 +01:00
Dennis Guse 32070a0bd6 Use java.time instead of custom code for parsing (requires API 26+).
This is a functional change on API26+ as a timezone identifier is required.
2020-01-11 19:31:58 +01:00
Dennis Guse b9420d4dde Removed LocationManagerConnector. 2020-01-11 19:31:58 +01:00
Dennis Guse 1b810e1294 TrackRecordingService: do not try to register location listener every minute. 2020-01-11 19:31:58 +01:00
Dennis Guse f16d59bff6 Cleanup. 2020-01-11 19:31:58 +01:00
Dennis Guse b5bf3aadc5 Refactored wake lock release. 2020-01-11 19:31:58 +01:00
Dennis Guse f6a27a1522 Documentation: add activities overview. 2020-01-11 19:31:58 +01:00
Dennis Guse 83758fcd72 Always cancel TrackRecordingService's notification. 2020-01-11 19:31:58 +01:00
Dennis Guse 070917bc0b Extracted TrackRecordingService's Binder. 2020-01-11 19:31:58 +01:00
Dennis Guse 543a04dfdd Cleanup. 2020-01-11 19:31:58 +01:00
Dennis Guse b4d5eb0eec Refactoring: SharedPreferences are not provided via DataSource anymore. 2020-01-11 19:31:58 +01:00
Dennis Guse 9f84cc4952 Use ContentUris.parseId(). 2020-01-11 19:31:58 +01:00
Dennis Guse 09fe35f1ca Cleanup. 2020-01-11 19:31:58 +01:00
Dennis Guse 27e50b2dd9 Moved ContentProvider into separate package. 2020-01-11 19:31:58 +01:00
Dennis Guse 971460848b Extracted LocationIterator from ContentProviderUtils.
Also refactored LocationFactory to TrackPointFactory.
2020-01-11 19:31:58 +01:00
Dennis Guse 8bc2937bb4 Extracted LocationIterator from ContentProviderUtils. 2020-01-11 19:31:58 +01:00
Dennis Guse 0e9b68c2a1 Cleanup. 2020-01-11 19:31:58 +01:00
Nathan Hearnsberger ba298c2afb Additional language support for export_with/without_photos (DE, ES). 2020-01-11 19:27:53 +01:00
Nathan Hearnsberger 79f5ed4dc3 Added KMZ without photos, photo details to export picker. 2020-01-11 19:27:53 +01:00
Dennis Guse bbb2f874ee Release v3.3.1 2020-01-03 18:11:57 +01:00
Dennis Guse 9af3759b07 Bugfix: app name (release) not shown.
Introduced in 6cc7271a41.
2020-01-03 17:54:15 +01:00
Dennis Guse 1f6fe6bf0c Android 10: on uninstall ask the user if the app data should be kept. 2020-01-03 17:50:07 +01:00
Dennis Guse 7b291e85d7 Renamed DeleteActivity to TrackDeleteActivity. 2020-01-03 17:04:51 +01:00
Dennis Guse 0f72066bc0 Add IntentChooser for dashboards. 2020-01-02 19:56:24 +01:00
Dennis Guse 70e0e7b216 Add README for data API. 2020-01-02 19:54:19 +01:00
Peter Storch 7e50bb1b6d Merge pull request #114 from pstorch/79_osm_plugin
integrate dashboard plugin
2020-01-02 17:56:07 +01:00
Peter Storch a3eb107068 integrate dashboard plugin mechanism
To be able to integrate the OSMDashboard https://github.com/OpenTracksApp/OSMDashboard,
which addresses #79. Also other Dashboards will be possible.
2020-01-02 17:45:01 +01:00
Dennis Guse 6133ef121a Release v3.3.0 2020-01-01 14:46:28 +01:00
Dennis Guse c6d24e9534 Pictures: removed MediaScanner. 2020-01-01 14:44:23 +01:00
Dennis Guse 441d3f721f Debug version: use a black logo. 2020-01-01 14:39:16 +01:00
Dennis Guse 3c212e4bcc Bugfix: for taking pictures; provide access to external/pictures. 2020-01-01 14:28:58 +01:00
Dennis Guse 50eb71bc1f Cleanup. 2020-01-01 14:16:02 +01:00
Dennis Guse 0806821b5f Fixed import/export message to show Storage Access Framework selected path. 2019-12-29 23:33:10 +01:00
Dennis Guse 36d0f17953 Removed french Congo translation (identical to values-fr). 2019-12-29 21:57:39 +01:00
Dennis Guse 700adb7972 Export dialog: removed "from folder" text. 2019-12-29 21:56:48 +01:00
Dennis Guse a156369f8b Import dialog: removed "from folder" text. 2019-12-29 21:32:01 +01:00
Dennis Guse c0f6dc511b Removed values-ar-rXB/strings.xml as it was for testing only. 2019-12-29 21:28:36 +01:00
Dennis Guse 63e1461eba Removed values-en-rXA/strings.xml as it was for testing only. 2019-12-29 20:59:30 +01:00
Dennis Guse 804e5f10c8 Removed values-bn/strings.xml as translations were invalid. 2019-12-29 20:53:37 +01:00
Dennis Guse 6cc7271a41 Build: debug version uses different icon and appLabel. 2019-12-29 20:46:46 +01:00
Dennis Guse f66194189b Removed translation description from values/strings.xml 2019-12-29 20:15:37 +01:00
Dennis Guse 5d97d6d492 Use Android 7.0 localization: merge latin american and present en-rGB as default for en. 2019-12-29 19:13:26 +01:00
Dennis Guse f3a308e669 BREAKING CHANGE: pictures are now stored in getExternalFilesDir(); no migration takes place! 2019-12-29 15:43:06 +01:00
Dennis Guse a230504925 Export & import via Storage Access Framework. 2019-12-29 15:41:28 +01:00
Dennis Guse 90e19e282c Upgraded tests to Junit4. 2019-12-29 15:31:53 +01:00
Dennis Guse b585bfe44c Moved to Github organization: OpenTracksApp. 2019-12-27 23:54:29 +01:00
Dennis Guse 475e04eeb4 Add PlantUML model for data structures. 2019-12-27 18:07:29 +01:00
Dennis Guse f89b2b0efb Removed TripStatists min/max latitude/longitude.
This also removed: search does not account for closer tracks (to current position).
2019-12-27 18:01:11 +01:00
Dennis Guse b94f3c176b Include unit in variable names. 2019-12-27 18:01:10 +01:00
Dennis Guse d1d729d287 Removed unused ContentProvider.getFirstTrackPointId(). 2019-12-27 18:01:07 +01:00
Dennis Guse e23bbcc405 Removed unnecessary Track.startId and Track.stopId. 2019-12-27 18:01:05 +01:00
Dennis Guse 4129985b0f Renamed SensorDataSetLocation to TrackPoint (similar to SQLite table). 2019-12-27 18:00:51 +01:00
Dennis Guse 5cb7b623d9 Removed TripStatistics from Waypoints. 2019-12-26 21:15:42 +01:00
Dennis Guse 3c52efadd6 Removed unused Waypoint.TripStatistics. 2019-12-26 21:15:41 +01:00
Dennis Guse 73b4977051 Removed unused Waypoint.startId and Waypoint.stopId. 2019-12-26 21:15:41 +01:00
Dennis Guse 422c2c02ce Assume GPS provides always time (removed workaround). 2019-12-26 21:15:41 +01:00
Dennis Guse d692329975 Settings: hide advanced recording settings.
And use switches rather than checkboxes.
2019-12-25 23:22:20 +01:00
193 changed files with 2546 additions and 14682 deletions
+21
View File
@@ -0,0 +1,21 @@
#https://github.com/marketplace/actions/android-emulator-runner
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: macos-latest
strategy:
matrix:
api-level: [21, 29]
target: [default]
steps:
- name: checkout
uses: actions/checkout@v2
- name: run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: x86_64
script: ./gradlew connectedCheck
+10 -10
View File
@@ -22,18 +22,18 @@ android:
- android-29
# Emulator Management: Create, Start and Wait
#install:
# - echo y | sdkmanager "platform-tools"
# - echo y | sdkmanager --channel=4 "emulator"
# - echo y | sdkmanager "system-images;android-$API;$EMU_FLAVOR;$ABI"
# - echo no | avdmanager create avd --force -n test -k "system-images;android-$API;$EMU_FLAVOR;$ABI" -c 10M
# - emulator -verbose -avd test -no-accel -no-snapshot -no-window -camera-back none -camera-front none -selinux permissive -qemu -m 2048 &
# - android-wait-for-emulator
# - adb shell input keyevent 82 &
install:
- echo y | sdkmanager "platform-tools"
- echo y | sdkmanager --channel=4 "emulator"
- echo y | sdkmanager "system-images;android-$API;$EMU_FLAVOR;$ABI"
- echo no | avdmanager create avd --force -n test -k "system-images;android-$API;$EMU_FLAVOR;$ABI" -c 10M
- emulator -verbose -avd test -no-accel -no-snapshot -no-window -camera-back none -camera-front none -selinux permissive -qemu -m 2048 &
- android-wait-for-emulator
- adb shell input keyevent 82 &
script:
- ./gradlew clean build
# - ./gradlew connectedCheck
- ./gradlew build
- ./gradlew connectedCheck
sbefore_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
+5 -2
View File
@@ -30,10 +30,10 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
## 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
* __Heart rate:__ supports Bluetooth LE sensors
* __Export data:__ export tracks either as KMZ (incl. photos), KML or GPX
* __No Internet access:__ Internet is not used
* __Voice annoucements__
* __Voice announcements__
* __No advertising__
* __Privacy:__
* _does not contain any in-app analytics_
@@ -47,6 +47,9 @@ _Please note:_
_Tracks_ 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
As of v3.3.1 OpenTracks enables to use custom dashboards.
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates).
## Screenshots
<div>
+30
View File
@@ -0,0 +1,30 @@
# OpenTracks Data API
## Reason
OpenTracks by itself only provides basic features for data visualization and also lacks functionality for analytics.
By itself OpenTracks provides a basic dashboard that can show summary of a track (while recording as well as afterwards).
Although this might be (mostly) sufficient, it has some limitations.
This include limited configurability (e.g., ordering of shown elements) and the lack of Internet access.
In fact, the later is considered a feature but severely limits the implementation presenting a map (at least in an easy way).
_NOTE:_ for showing a track on a map a workaround was implemented in OpenTracks.
Data could be exported as KMZ and presented in 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)).
For the time being, the OpenTracks Data API can be used to implement mainly custom dashboards.
## Implementation
OpenTracks by default does not expose any data to 3rd-party applications.
On the request of the user (i.e., pressing a button), an Intent is created that exposes the `Track` data (it's summary) as well as the `TrackPoint`s of one or more selected tracks.
For each, one URI is provided.
These URIs notify observers if on content changes (e.g., a new `TrackPoint` was added).
Limitations:
* only tracks selected by the user are exposed,
* `Waypoints` are not exposed,
* access is only granted temporarily and automatically revoked (see `FLAG_GRANT_READ_URI_PERMISSION`),
* no write access is possible, and
* neither track recordings can be started, paused, or stopped.
All required information can be found in `IntentDashboardUtils.java`.
A _reference implementation_ is the [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard).
+12 -2
View File
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.2'
classpath 'com.android.tools.build:gradle:3.5.3'
}
}
@@ -54,6 +54,11 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
manifestPlaceholders = [
applicationLogo: "@drawable/ic_logo_color_24dp",
applicationName: "@string/app_name"
]
//Useful for debugging.
//versionCode getVersionCode()
@@ -69,6 +74,10 @@ android {
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
manifestPlaceholders = [
applicationLogo: "@drawable/ic_logo_black_24dp",
applicationName: "OpenTracks (Debug)"
]
}
release {
@@ -89,6 +98,7 @@ android {
dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
@@ -100,5 +110,5 @@ dependencies {
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'org.mockito:mockito-core:3.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'com.google.android.material:material:1.1.0'
}
+40
View File
@@ -0,0 +1,40 @@
@startuml
title
State of OpenTracks
end title
legend right
SettingsActivity can be reached
from every activity showing a menu.
endlegend
[*] --> TrackListActivity
TrackListActivity --> TrackDetailActivity : Start recording
TrackListActivity --> SearchListActivity
TrackListActivity --> MarkerListActivity : Show all markers
TrackListActivity --> TrackDetailActivity : Show track
TrackListActivity --> TrackDeleteActivity
SearchListActivity --> MarkerDetailActivity : Search result
SearchListActivity --> TrackDetailActivity : Search result
TrackDetailActivity --> TrackDeleteActivity
TrackDetailActivity --> TrackEditActivity
TrackDetailActivity --> MarkerListActivity
TrackDetailActivity --> MarkerEditActivity : Add marker
TrackDetailActivity --> CameraIntent : Add marker (with photo)
TrackDetailActivity --> ShareFileIntent
TrackDetailActivity --> DashboardIntent : Database cursor
TrackDetailActivity --> TrackOnMapIntent : KMZ
MarkerListActivity --> MarkerDetailActivity
MarkerDetailActivity --> MarkerEditActivity
MarkerDetailActivity --> CoordinateOnMapIntent : URI
SettingsActivity --> AboutActivity
SettingsActivity --> HelpActivity
SettingsActivity --> ExportActivity
SettingsActivity --> ImportActivity
@enduml
+52
View File
@@ -0,0 +1,52 @@
@startuml
class Track {
-id
-name
-description
-category
-numberOfPoints
-icon
}
Track o-- TrackPoint
Track o-- Waypoint
Track "1" - "1" TripStatistics
class TripStatistics {
- startTime
- stopTime
- totalDistance
- totalTime
- movingTime
- maxSpeed
- totalElevationGain
}
class TrackPoint extends android.location.Location {
- id
- trackId
- longitude
- latitude
- time
- altitude
- accuracy
- speed
- bearing
- sensor_heartrate
- sensor_cadence
- sensor_power
}
class Waypoint {
- id
- trackId
- name
- description
- category
- icon
- length
- duration
- location
- tripStatistics
- photoUrl
}
@enduml
@@ -0,0 +1,13 @@
v3.3.0: OpenTracks
Breaking changes:
- Pictures are now stored in app-private storage (do not show up other picture apps)
NOTE: Pictures taking with previous version cannot be shown
- Localization for Spanish (Latin America) only works on Android 7.0 or later
Changes:
- Import/Export via Storage Access Framework (avoids READ/WRITE_EXTERNAL_STORAGE permission)
Developer:
- Cleaned internal data structures
- Use separate icons/app names for debug version
@@ -0,0 +1,12 @@
v3.3.1: OpenTracks
Changes:
- Android 10: on uninstall allow to keep app data
- Introduced Dashboard API: map icon can be used to open current track in external app
(requires installation of another app like OSMDashboard)
Bugfix:
- App label was broken (introduced in v3.3.0)
Developer:
- Documentation for Dashboard API can be found in README_DEVELOPER.md
@@ -0,0 +1,11 @@
v3.3.2: OpenTracks
Changes:
- Export: overwrites files now
Bugfix:
- Always remove recording notification
Developer:
- On API 26+ java.time is used for parsing and writing dates
- Refactoring of TrackRecordingService
@@ -0,0 +1,12 @@
v3.4.0: OpenTracks
Changes:
- Dashboard API: multiple tracks
- Updated help
Bugfix:
- Fixed crash on fragment re-creation
- When paused: prevent stopping app
Developer:
- Enable testing via CI (Github Actions and TravisCI)
@@ -18,8 +18,8 @@ package de.dennisguse.opentracks;
import android.location.Location;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
/**
@@ -60,16 +60,16 @@ public class TrackStubUtils {
*
* @return a track stub.
*/
public static SensorDataSetLocation createSensorDataSetLocation() {
public static TrackPoint createSensorDataSetLocation() {
return createSensorDataSetLocation(INITIAL_LATITUDE, INITIAL_LONGITUDE, INITIAL_ALTITUDE);
}
/**
* Creates a {@link SensorDataSetLocation} stub with specified values.
* Creates a {@link TrackPoint} stub with specified values.
*
* @return a SensorDataSetLocation stub.
*/
private static SensorDataSetLocation createSensorDataSetLocation(double latitude, double longitude, double altitude) {
private static TrackPoint createSensorDataSetLocation(double latitude, double longitude, double altitude) {
// Initial Location
Location loc = new Location(LOCATION_PROVIDER);
loc.setLatitude(latitude);
@@ -81,6 +81,6 @@ public class TrackStubUtils {
loc.setBearing(INITIAL_BEARING);
SensorDataSet sd = new SensorDataSet(Float.NaN, Float.NaN);
return new SensorDataSetLocation(loc, sd);
return new TrackPoint(loc, sd);
}
}
@@ -28,7 +28,6 @@ import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.StringUtils;
@@ -70,7 +69,7 @@ public class DescriptionGeneratorTest {
track.setTripStatistics(stats);
track.setCategory("hiking");
String expected = //"Created by"
"<a href='https://github.com/dennisguse/opentracks'>OpenTracks</a><p>"
"<a href='https://github.com/OpenTracksApp/OpenTracks'>OpenTracks</a><p>"
+ "Name: -<br>"
+ "Activity type: hiking<br>"
+ "Description: -<br>"
@@ -93,41 +92,6 @@ public class DescriptionGeneratorTest {
Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
/**
* Tests {@link DescriptionGenerator#generateWaypointDescription(TripStatistics)}.
*/
@Test
public void testGenerateWaypointDescription() {
Waypoint waypoint = new Waypoint();
TripStatistics stats = new TripStatistics();
stats.setTotalDistance(20000);
stats.setTotalTime(600000);
stats.setMovingTime(300000);
stats.setMaxSpeed(100);
stats.setMaxElevation(550);
stats.setMinElevation(-500);
stats.setTotalElevationGain(6000);
stats.setMaxGrade(0.42);
stats.setMinGrade(0.11);
stats.setStartTime(START_TIME);
waypoint.setTripStatistics(stats);
String expected = "Total distance: 20.00 km (12.4 mi)\n"
+ "Total time: 10:00\n"
+ "Moving time: 05:00\n"
+ "Average speed: 120.00 km/h (74.6 mi/h)\n"
+ "Average moving speed: 240.00 km/h (149.1 mi/h)\n"
+ "Max speed: 360.00 km/h (223.7 mi/h)\n"
+ "Average pace: 0:30 min/km (0:48 min/mi)\n"
+ "Average moving pace: 0:15 min/km (0:24 min/mi)\n"
+ "Fastest pace: 0:10 min/km (0:16 min/mi)\n"
+ "Max elevation: 550 m (1804 ft)\n"
+ "Min elevation: -500 m (-1640 ft)\n"
+ "Elevation gain: 6000 m (19685 ft)\n"
+ "Max grade: 42 %\n"
+ "Min grade: 11 %\n"
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "\n";
Assert.assertEquals(expected, descriptionGenerator.generateWaypointDescription(waypoint.getTripStatistics()));
}
/**
* Tests {@link DescriptionGenerator#writeDistance(double, StringBuilder, int, String)}.
@@ -38,6 +38,7 @@ import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.stats.TripStatistics;
/**
@@ -82,10 +83,6 @@ public class SearchEngineTest {
stats.setStopTime(NOW);
}
int latitude = (int) ((HERE.getLatitude() + distance) * 1E6);
int longitude = (int) ((HERE.getLongitude() + distance) * 1E6);
stats.setBounds(latitude, longitude, latitude, longitude);
Uri uri = providerUtils.insertTrack(track);
return ContentUris.parseId(uri);
}
@@ -211,20 +208,6 @@ public class SearchEngineTest {
assertWaypointResult(descriptionWaypointId, results.get(2));
}
@Test
public void testSearchTrackDistance() {
// All results match text, but they're at difference distances from the user.
long farFarAwayId = insertTrack("aa", 0.3);
long nearId = insertTrack("ab", 0.1);
long farId = insertTrack("ac", 0.2);
SearchQuery query = new SearchQuery("a", HERE, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Distance order.
assertTrackResults(results, nearId, farId, farFarAwayId);
}
@Test
public void testSearchWaypointDistance() {
// All results match text, but they're at difference distances from the user.
@@ -14,8 +14,9 @@
* the License.
*/
package de.dennisguse.opentracks.content;
package de.dennisguse.opentracks.content.provider;
import android.content.ContentUris;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
@@ -26,7 +27,6 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import de.dennisguse.opentracks.content.CustomContentProvider.DatabaseHelper;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
@@ -47,13 +47,13 @@ public class CustomContentProviderTest {
@Before
public void setUp() {
context.deleteDatabase(DATABASE_NAME);
db = (new DatabaseHelper(context, DATABASE_NAME)).getWritableDatabase();
db = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getWritableDatabase();
customContentProvider = new CustomContentProvider() {
};
}
/**
* Tests {@link CustomContentProvider.DatabaseHelper#onCreate(SQLiteDatabase)}.
* Tests {@link CustomSQLiteOpenHelper#onCreate(SQLiteDatabase)}.
*/
@Test
public void testDatabaseHelper_OnCreate() {
@@ -76,8 +76,13 @@ public class CustomContentProviderTest {
@Test
public void testGetType() {
Assert.assertEquals(TracksColumns.CONTENT_TYPE, customContentProvider.getType(TracksColumns.CONTENT_URI));
Assert.assertEquals(TrackPointsColumns.CONTENT_TYPE, customContentProvider.getType(TrackPointsColumns.CONTENT_URI));
Assert.assertEquals(TracksColumns.CONTENT_ITEMTYPE, customContentProvider.getType(ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), 1).build()));
Assert.assertEquals(TrackPointsColumns.CONTENT_TYPE, customContentProvider.getType(TrackPointsColumns.CONTENT_URI_BY_ID));
Assert.assertEquals(TrackPointsColumns.CONTENT_ITEMTYPE, customContentProvider.getType(ContentUris.appendId(TrackPointsColumns.CONTENT_URI_BY_TRACKID.buildUpon(), 1).build()));
Assert.assertEquals(WaypointsColumns.CONTENT_TYPE, customContentProvider.getType(WaypointsColumns.CONTENT_URI));
Assert.assertEquals(WaypointsColumns.CONTENT_ITEMTYPE, customContentProvider.getType(ContentUris.appendId(WaypointsColumns.CONTENT_URI.buildUpon(), 1).build()));
}
/**
@@ -142,7 +147,7 @@ public class CustomContentProviderTest {
createTable(TrackPointsColumns.TABLE_NAME);
createTable(WaypointsColumns.TABLE_NAME);
DatabaseHelper databaseHelper = new DatabaseHelper(context);
databaseHelper.onUpgrade(db, oldVersion, CustomContentProvider.DATABASE_VERSION);
CustomSQLiteOpenHelper databaseHelper = new CustomSQLiteOpenHelper(context);
databaseHelper.onUpgrade(db, oldVersion, CustomSQLiteOpenHelper.DATABASE_VERSION);
}
}
@@ -13,7 +13,7 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.content;
package de.dennisguse.opentracks.content.provider;
import android.content.ContentResolver;
import android.content.ContentUris;
@@ -37,6 +37,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import de.dennisguse.opentracks.content.data.TestDataUtil;
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.TracksColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
@@ -75,16 +76,16 @@ public class CustomContentProviderUtilsTest {
@Test
public void testLocationIterator_noPoints() {
testIterator(1, 0, 1, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(1, 0, 1, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_customFactory() {
final Location location = new Location("test_location");
final TrackPoint location = new TrackPoint("test_location");
final AtomicInteger counter = new AtomicInteger();
testIterator(1, 15, 4, false, new LocationFactory() {
testIterator(1, 15, 4, false, new TrackPointFactory() {
@Override
public Location createLocation() {
public TrackPoint createLocation() {
counter.incrementAndGet();
return location;
}
@@ -105,45 +106,45 @@ public class CustomContentProviderUtilsTest {
@Test
public void testLocationIterator_noBatchAscending() {
testIterator(1, 50, 100, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 100, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_noBatchDescending() {
testIterator(1, 50, 100, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 100, true, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, true, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_batchAscending() {
testIterator(1, 50, 11, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 11, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_batchDescending() {
testIterator(1, 50, 11, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 11, true, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, true, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_largeTrack() {
testIterator(1, 20000, 2000, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(1, 20000, 2000, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
}
private List<Location> testIterator(long trackId, int numPoints, int batchSize, boolean descending, LocationFactory locationFactory) {
private List<Location> testIterator(long trackId, int numPoints, int batchSize, boolean descending, TrackPointFactory trackPointFactory) {
long lastPointId = initializeTrack(trackId, numPoints);
((ContentProviderUtils) contentProviderUtils).setDefaultCursorBatchSize(batchSize);
List<Location> locations = new ArrayList<Location>(numPoints);
try (LocationIterator it = contentProviderUtils.getTrackPointLocationIterator(trackId, -1L, descending, locationFactory)) {
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(trackId, -1L, descending, trackPointFactory)) {
while (it.hasNext()) {
Location loc = it.next();
Assert.assertNotNull(loc);
locations.add(loc);
// Make sure the IDs are returned in the right order.
Assert.assertEquals(descending ? lastPointId - locations.size() + 1
: lastPointId - numPoints + locations.size(), it.getLocationId());
: lastPointId - numPoints + locations.size(), it.getTrackPointId());
}
Assert.assertEquals(numPoints, locations.size());
}
@@ -173,10 +174,10 @@ public class CustomContentProviderUtilsTest {
// Load all inserted locations.
long lastPointId = -1;
int counter = 0;
try (LocationIterator it = contentProviderUtils.getTrackPointLocationIterator(id, -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(id, -1L, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY)) {
while (it.hasNext()) {
it.next();
lastPointId = it.getLocationId();
lastPointId = it.getTrackPointId();
counter++;
}
}
@@ -229,7 +230,7 @@ public class CustomContentProviderUtilsTest {
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, null, null, null, TrackPointsColumns._ID);
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());
@@ -238,7 +239,7 @@ public class CustomContentProviderUtilsTest {
// 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, null, null, null, TrackPointsColumns._ID);
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());
@@ -264,7 +265,7 @@ public class CustomContentProviderUtilsTest {
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, null, null, null, TrackPointsColumns._ID);
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
Assert.assertEquals(20, tracksPointsCursor.getCount());
Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
Assert.assertEquals(1, waypointCursor.getCount());
@@ -273,7 +274,7 @@ public class CustomContentProviderUtilsTest {
// 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, null, null, null, TrackPointsColumns._ID);
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(0, waypointCursor.getCount());
@@ -336,7 +337,6 @@ public class CustomContentProviderUtilsTest {
public void testCreateContentValues_waypoint() {
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
// Bottom
long startTime = 1000L;
// AverageSpeed
@@ -353,11 +353,13 @@ public class CustomContentProviderUtilsTest {
statistics.setMinElevation(1200.0);
statistics.setMaxGrade(15.0);
statistics.setMinGrade(minGrade);
statistics.setBounds(-10000, 20000, 30000, -40000);
track.setTripStatistics(statistics);
contentProviderUtils.insertTrack(track);
// Insert at first.
Waypoint waypoint = new Waypoint();
waypoint.setDescription(TEST_DESC);
waypoint.setTripStatistics(statistics);
Location location = new Location("test");
location.setLatitude(22);
@@ -375,8 +377,6 @@ public class CustomContentProviderUtilsTest {
Assert.assertEquals(waypointId, contentValues.get(WaypointsColumns._ID));
Assert.assertEquals(22 * 1000000, contentValues.get(WaypointsColumns.LONGITUDE));
Assert.assertEquals(TEST_DESC, contentValues.get(WaypointsColumns.DESCRIPTION));
Assert.assertEquals(startTime, contentValues.get(WaypointsColumns.STARTTIME));
Assert.assertEquals(minGrade, contentValues.get(WaypointsColumns.MINGRADE));
}
/**
@@ -415,7 +415,7 @@ public class CustomContentProviderUtilsTest {
/**
* Tests the method
* {@link ContentProviderUtils#deleteWaypoint(Context, long)}
* {@link ContentProviderUtils#deleteWaypoint(long)}
* when there is only one waypoint in the track.
*/
@Test
@@ -431,21 +431,20 @@ public class CustomContentProviderUtilsTest {
contentProviderUtils.insertWaypoint(waypoint1);
// Delete
contentProviderUtils.deleteWaypoint(context, 1);
contentProviderUtils.deleteWaypoint(1);
Assert.assertNull(contentProviderUtils.getWaypoint(1));
}
/**
* Tests the method
* {@link ContentProviderUtils#deleteWaypoint(Context, long)}
* {@link ContentProviderUtils#deleteWaypoint(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);
contentProviderUtils.insertTrack(track);
TripStatistics statistics = new TripStatistics();
statistics.setStartTime(1000L);
@@ -459,24 +458,25 @@ public class CustomContentProviderUtilsTest {
statistics.setMinElevation(1200.0);
statistics.setMaxGrade(15.0);
statistics.setMinGrade(-25.0);
statistics.setBounds(-10000, 20000, 30000, -40000);
track.setTripStatistics(statistics);
contentProviderUtils.insertTrack(track);
// Insert at first.
Waypoint waypoint1 = new Waypoint();
waypoint1.setDescription(MOCK_DESC);
waypoint1.setTrackId(trackId);
waypoint1.setTripStatistics(statistics);
long waypoint1Id = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint1));
Waypoint waypoint2 = new Waypoint();
waypoint2.setDescription(MOCK_DESC);
waypoint2.setTrackId(trackId);
waypoint2.setTripStatistics(statistics);
long waypoint2Id = ContentUris.parseId(contentProviderUtils.insertWaypoint(waypoint2));
// Delete
Assert.assertNotNull(contentProviderUtils.getWaypoint(waypoint1Id));
contentProviderUtils.deleteWaypoint(context, waypoint1Id);
contentProviderUtils.deleteWaypoint(waypoint1Id);
Assert.assertNull(contentProviderUtils.getWaypoint(waypoint1Id));
Assert.assertEquals(MOCK_DESC, contentProviderUtils.getWaypoint(waypoint2Id).getDescription());
@@ -507,32 +507,6 @@ public class CustomContentProviderUtilsTest {
Assert.assertEquals(4, contentProviderUtils.getNextWaypointNumber(trackId));
}
/**
* Tests the method
* {@link ContentProviderUtils#getLastWaypoint(long)}.
*/
@Test
public void testGetLastWaypoint() {
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
Waypoint waypoint1 = new Waypoint();
waypoint1.setTrackId(trackId);
waypoint1.setDescription("Desc1");
Waypoint waypoint2 = new Waypoint();
waypoint2.setTrackId(trackId);
waypoint2.setDescription("Desc2");
Waypoint waypoint3 = new Waypoint();
waypoint3.setTrackId(trackId);
waypoint3.setDescription("Desc3");
contentProviderUtils.insertWaypoint(waypoint1);
contentProviderUtils.insertWaypoint(waypoint2);
contentProviderUtils.insertWaypoint(waypoint3);
Assert.assertEquals("Desc3", contentProviderUtils.getLastWaypoint(trackId).getDescription());
}
/**
* Tests the method {@link ContentProviderUtils#insertWaypoint(Waypoint)} and
* {@link ContentProviderUtils#getWaypoint(long)}.
@@ -663,32 +637,6 @@ public class CustomContentProviderUtilsTest {
Assert.assertEquals(11, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
}
/**
* Tests the method {@link ContentProviderUtils#getFirstTrackPointId(long)}.
*/
@Test
public void testGetFirstTrackPointId() {
// Insert track, point at first.
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
Assert.assertNotEquals(-1L, contentProviderUtils.getFirstTrackPointId(trackId));
}
/**
* Tests the method {@link ContentProviderUtils#getLastTrackPointId(long)}.
*/
@Test
public void testGetLastTrackPointId() {
// Insert track, point at first.
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
Assert.assertTrue(contentProviderUtils.getFirstTrackPointId(trackId) < contentProviderUtils.getLastTrackPointId(trackId));
}
/**
* Tests the method {@link ContentProviderUtils#getLastValidTrackPoint(long)}.
*/
@@ -742,7 +690,7 @@ public class CustomContentProviderUtilsTest {
}
/**
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in descending.
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, TrackPointFactory)} in descending.
*/
@Test
public void testGetTrackPointLocationIterator_desc() {
@@ -758,18 +706,18 @@ public class CustomContentProviderUtilsTest {
long startTrackPointId = trackpointIds[9];
LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next();
Assert.assertEquals(startTrackPointId - i, locationIterator.getLocationId());
Assert.assertEquals(startTrackPointId - i, locationIterator.getTrackPointId());
checkLocation((trackpointIds.length - 1) - i, location);
}
Assert.assertFalse(locationIterator.hasNext());
}
/**
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in ascending.
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, TrackPointFactory)} in ascending.
*/
@Test
public void testGetTrackPointLocationIterator_asc() {
@@ -785,11 +733,11 @@ public class CustomContentProviderUtilsTest {
long startTrackPointId = trackpointIds[0];
LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next();
Assert.assertEquals(startTrackPointId + i, locationIterator.getLocationId());
Assert.assertEquals(startTrackPointId + i, locationIterator.getTrackPointId());
checkLocation(i, location);
}
@@ -818,4 +766,12 @@ public class CustomContentProviderUtilsTest {
contentProviderUtils.insertTrack(track);
contentProviderUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), track.getLocations().size(), track.getId());
}
@Test
public void testFormatIdListForUri() {
Assert.assertEquals("", ContentProviderUtils.formatIdListForUri(new long[]{}));
Assert.assertEquals("12", ContentProviderUtils.formatIdListForUri(new long[]{12}));
Assert.assertEquals("42,43,44", ContentProviderUtils.formatIdListForUri(new long[]{42, 43, 44}));
}
}
@@ -31,7 +31,7 @@ import org.junit.runner.RunWith;
import de.dennisguse.opentracks.TrackStubUtils;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -61,8 +61,7 @@ public class ChartFragmentTest {
@Before
public void setUp() {
boolean chartByDistance = false;
chartFragment = new ChartFragment(chartByDistance) {
};
chartFragment = (ChartFragment) ChartFragment.newInstance(chartByDistance);
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
chartFragment.setTripStatisticsUpdater(TrackStubUtils.INITIAL_TIME);
}
@@ -72,7 +71,7 @@ public class ChartFragmentTest {
*/
@Test
public void testFillDataPoint_sensorIncorrect() {
SensorDataSetLocation sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
// No input.
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
@@ -96,7 +95,7 @@ public class ChartFragmentTest {
*/
@Test
public void testFillDataPoint_sensorCorrect() {
SensorDataSetLocation sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
// No input.
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(Float.NaN, point[ChartView.HEART_RATE_SERIES + 1], 0.01);
@@ -123,17 +122,17 @@ public class ChartFragmentTest {
// By distance.
chartFragment.setChartByDistance(true);
// Resets last location and writes first location.
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0], 0.01);
// The second is a same location, just different time.
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(0.0, point[0], 0.01);
// The third location is a new location, and use metric.
SensorDataSetLocation sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation3.setLatitude(23);
point = fillDataPointTestHelper(sensorDataSetLocation3);
@@ -145,7 +144,7 @@ public class ChartFragmentTest {
Assert.assertEquals(distance1, point[0], 0.01);
// The fourth location is a new location, and use metric.
SensorDataSetLocation sensorDataSetLocation4 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation4 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation4.setLatitude(24);
point = fillDataPointTestHelper(sensorDataSetLocation4);
@@ -167,12 +166,12 @@ public class ChartFragmentTest {
chartFragment.setMetricUnits(false);
// The first is a same location, just different time.
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0], 0.01);
// The second location is a new location, and use imperial.
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setLatitude(23);
point = fillDataPointTestHelper(sensorDataSetLocation2);
@@ -186,7 +185,7 @@ public class ChartFragmentTest {
Assert.assertEquals(distance1, point[0], 0.01);
// The third location is a new location, and use imperial.
SensorDataSetLocation sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation3.setLatitude(24);
point = fillDataPointTestHelper(sensorDataSetLocation3);
@@ -206,11 +205,11 @@ public class ChartFragmentTest {
public void testFillDataPoint_time() {
// By time
chartFragment.setChartByDistance(false);
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0], 0.01);
long timeSpan = 222;
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + timeSpan);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals((double) timeSpan, point[0], 0.01);
@@ -221,7 +220,7 @@ public class ChartFragmentTest {
*/
@Test
public void testFillDataPoint_elevation() {
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
/*
* At first, clear old points of elevation, so give true to the second parameter.
@@ -233,7 +232,7 @@ public class ChartFragmentTest {
/*
* Send another value to buffer, now there are two values, INITIAL_ALTITUDE and INITIAL_ALTITUDE * 2.
*/
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0,
@@ -251,7 +250,7 @@ public class ChartFragmentTest {
* At first, clear old points of speed, so give true to the second parameter.
* It will not be filled in to the speed buffer.
*/
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(128.5f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
@@ -260,7 +259,7 @@ public class ChartFragmentTest {
* Tests the logic when both metricUnits and reportSpeed are true.
* This location will be filled into speed buffer.
*/
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/*
* Add a time span here to make sure the second point is valid, the value 222 here is doesn't matter.
@@ -280,12 +279,12 @@ public class ChartFragmentTest {
chartFragment.setMetricUnits(false);
// First data point is not added to the speed buffer
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(100.0f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/*
* Add a time span here to make sure the second point and the speed is valid.
@@ -306,12 +305,12 @@ public class ChartFragmentTest {
chartFragment.setReportSpeed(false);
// First data point is not added to the speed buffer
SensorDataSetLocation sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(100.0f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
SensorDataSetLocation sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
/*
* Add a time span here to make sure the second point and the speed is valid.
@@ -330,7 +329,7 @@ public class ChartFragmentTest {
public void testFillDataPoint_pace_zeroSpeed() {
// Setups reportSpeed to false.
chartFragment.setReportSpeed(false);
SensorDataSetLocation sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
TrackPoint sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation.setSpeed(0);
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(0.0, point[ChartView.PACE_SERIES + 1], 0.01);
@@ -32,11 +32,11 @@ import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.SimpleTimeZone;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.data.Track;
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.provider.ContentProviderUtils;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
@@ -57,9 +57,9 @@ public abstract class AbstractTestFileTrackImporter {
static final double TRACK_ELEVATION = 324.0;
static final String TRACK_TIME_0 = "2010-04-22T18:21:00Z";
static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123";
static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123";
static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123";
static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123Z";
static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123Z";
static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123Z";
static final SimpleDateFormat DATE_FORMAT_0 = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US);
static final SimpleDateFormat DATE_FORMAT_1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
@@ -101,8 +101,6 @@ public abstract class AbstractTestFileTrackImporter {
*/
protected void expectFirstTrackPoint(Location location, long trackId, long trackPointId) {
when(contentProviderUtils.bulkInsertTrackPoint(location != null ? (Location[]) any() : (Location[]) any(), eq(1), eq(trackId))).thenReturn(1);
when(contentProviderUtils.getFirstTrackPointId(trackId)).thenReturn(trackPointId);
when(contentProviderUtils.getLastTrackPointId(trackId)).thenReturn(trackPointId);
}
/**
@@ -128,7 +126,5 @@ public abstract class AbstractTestFileTrackImporter {
if (time != -1L) {
Assert.assertEquals(time, track.getTripStatistics().getStartTime());
}
Assert.assertNotSame(-1, track.getStartId());
Assert.assertNotSame(-1, track.getStopId());
}
}
@@ -19,10 +19,10 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
@@ -208,8 +208,6 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
// For the following, use StubReturn since we don't care whether they are invoked or not.
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), anyInt(), anyLong())).thenReturn(1);
when(contentProviderUtils.getFirstTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_0);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
contentProviderUtils.deleteTrack(context, TRACK_ID_0);
@@ -15,14 +15,19 @@
*/
package de.dennisguse.opentracks.services;
import junit.framework.TestCase;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Tests the {@link AdaptiveLocationListenerPolicy}.
*
* @author youtaol
*/
public class AdaptiveLocationListenerPolicyTest extends TestCase {
@RunWith(JUnit4.class)
public class AdaptiveLocationListenerPolicyTest {
private static final long MIN = 1000;
private static final long MAX = 3000;
@@ -31,12 +36,11 @@ public class AdaptiveLocationListenerPolicyTest extends TestCase {
private static final long NEW_IDLE_TIME_NORMAL = 5000;
private static final long NEW_IDLE_TIME_SMALL = 2000;
private static final long NEW_IDLE_TIME_LESS_THAN_MIN = 500;
private AdaptiveLocationListenerPolicy adocationListenerPolicy;
private AdaptiveLocationListenerPolicy adaptiveLocationListenerPolicy;
@Override
protected void setUp() throws Exception {
super.setUp();
adocationListenerPolicy = new AdaptiveLocationListenerPolicy(MIN, MAX, MIN_DISTANCE);
@Before
public void setUp() {
adaptiveLocationListenerPolicy = new AdaptiveLocationListenerPolicy(MIN, MAX, MIN_DISTANCE);
}
/**
@@ -48,25 +52,27 @@ public class AdaptiveLocationListenerPolicyTest extends TestCase {
* <li>The newIdleTime is smaller than the smallest interval unit.</li>
* </ul>
*/
@Test
public void testGetDesiredPollingInterval() {
adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_BIG);
assertEquals(MAX, adocationListenerPolicy.getDesiredPollingInterval());
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_BIG);
Assert.assertEquals(MAX, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_NORMAL);
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_NORMAL);
// First get the half of NEW_IDLE_TIME_NORMAL, and then round it to the nearest second.
assertEquals((NEW_IDLE_TIME_NORMAL / 2 / 1000) * 1000, adocationListenerPolicy.getDesiredPollingInterval());
Assert.assertEquals((NEW_IDLE_TIME_NORMAL / 2 / 1000) * 1000, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_SMALL);
assertEquals(MIN, adocationListenerPolicy.getDesiredPollingInterval());
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_SMALL);
Assert.assertEquals(MIN, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
adocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_LESS_THAN_MIN);
assertEquals(MIN, adocationListenerPolicy.getDesiredPollingInterval());
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_LESS_THAN_MIN);
Assert.assertEquals(MIN, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
}
/**
* Tests the method {@link AdaptiveLocationListenerPolicy#getMinDistance_m()}.
*/
@Test
public void testGetMinDistance() {
assertEquals(MIN_DISTANCE, adocationListenerPolicy.getMinDistance_m());
Assert.assertEquals(MIN_DISTANCE, adaptiveLocationListenerPolicy.getMinDistance_m());
}
}
@@ -42,10 +42,10 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.CustomContentProvider;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -95,7 +95,7 @@ public class TrackRecordingServiceTest {
@After
public void tearDown() throws TimeoutException {
// Reset service (if some previous test failed)
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
if (service.isRecording() || service.isPaused()) {
service.endCurrentTrack();
}
@@ -126,7 +126,7 @@ public class TrackRecordingServiceTest {
Intent startIntent = createStartIntent(context);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(startIntent));
// Test if we start in no-recording mode by default.
Assert.assertFalse(service.isRecording());
@@ -138,7 +138,7 @@ public class TrackRecordingServiceTest {
public void testRecording_oldTracks() throws Exception {
createDummyTrack(trackId, -1L, false);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@@ -147,7 +147,7 @@ public class TrackRecordingServiceTest {
@Test
public void testRecording_orphanedRecordingTrack() throws Exception {
Intent startIntent = createStartIntent(context);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(startIntent));
// Just set recording track to a bogus value.
// Make sure that the service will not start recording and will clear the bogus track.
@@ -161,7 +161,7 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testStartNewTrack_alreadyRecording() throws Exception {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
Assert.assertTrue(service.isRecording());
long trackId = service.getRecordingTrackId();
@@ -178,7 +178,7 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testEndCurrentTrack_noRecording() throws Exception {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
// Ending the current track when there is no recording should not result in any error.
@@ -210,7 +210,7 @@ public class TrackRecordingServiceTest {
/**
* Inserts a location and waits for 200ms.
*/
private void insertLocation(ITrackRecordingService trackRecordingService) throws InterruptedException {
private void insertLocation(TrackRecordingServiceInterface trackRecordingService) throws InterruptedException {
Location location = new Location("gps");
location.setLongitude(35.0f);
location.setLatitude(45.0f);
@@ -226,7 +226,7 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
long waypointId = service.insertWaypoint(null, null, null, null);
@@ -236,7 +236,7 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testInsertWaypointMarker_validWaypoint() throws Exception {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
Assert.assertTrue(service.isRecording());
insertLocation(service);
@@ -250,7 +250,6 @@ public class TrackRecordingServiceTest {
Assert.assertEquals(trackId, wpt.getTrackId());
Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation());
Assert.assertNull(wpt.getTripStatistics());
service.endCurrentTrack();
}
@@ -25,9 +25,9 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.CustomContentProvider;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -47,7 +47,7 @@ public class TrackRecordingServiceTestLooper {
private Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils providerUtils;
private ContentProviderUtils contentProviderUtils;
@BeforeClass
public static void preSetUp() {
@@ -67,26 +67,26 @@ public class TrackRecordingServiceTestLooper {
};
customContentProvider.attachInfo(context, null);
providerUtils = new ContentProviderUtils(context);
contentProviderUtils = new ContentProviderUtils(context);
// Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().apply();
// Ensure that the database is empty before every test
providerUtils.deleteAllTracks(context);
contentProviderUtils.deleteAllTracks(context);
}
@After
public void tearDown() throws TimeoutException {
// Reset service (if some previous test failed)
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
if (service.isRecording() || service.isPaused()) {
service.endCurrentTrack();
}
// Ensure that the database is empty after every test
providerUtils.deleteAllTracks(context);
contentProviderUtils.deleteAllTracks(context);
}
@MediumTest
@@ -186,20 +186,20 @@ public class TrackRecordingServiceTestLooper {
@MediumTest
@Test
public void testIntegration_completeRecordingSession() throws TimeoutException {
List<Track> tracks = providerUtils.getAllTracks();
List<Track> tracks = contentProviderUtils.getAllTracks();
Assert.assertTrue(tracks.isEmpty());
fullRecordingSession();
}
private void fullRecordingSession() throws TimeoutException {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
Assert.assertFalse(service.isRecording());
// Start a track.
long id = service.startNewTrack();
Assert.assertTrue(id >= 0);
Assert.assertTrue(service.isRecording());
Track track = providerUtils.getTrack(id);
Track track = contentProviderUtils.getTrack(id);
Assert.assertNotNull(track);
Assert.assertEquals(id, track.getId());
Assert.assertEquals(id, PreferencesUtils.getRecordingTrackId(context));
@@ -226,7 +226,7 @@ public class TrackRecordingServiceTestLooper {
service.endCurrentTrack();
Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId());
track = providerUtils.getTrack(id);
track = contentProviderUtils.getTrack(id);
Assert.assertNotNull(track);
Assert.assertEquals(id, track.getId());
TripStatistics tripStatistics = track.getTripStatistics();
@@ -15,7 +15,11 @@
*/
package de.dennisguse.opentracks.stats;
import junit.framework.TestCase;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Tests for {@link TripStatistics}.
@@ -23,26 +27,17 @@ import junit.framework.TestCase;
*
* @author Rodrigo Damazio
*/
public class TripStatisticsTest extends TestCase {
@RunWith(JUnit4.class)
public class TripStatisticsTest {
private TripStatistics statistics;
@Override
protected void setUp() throws Exception {
super.setUp();
@Before
public void setUp() {
statistics = new TripStatistics();
}
public void testSetBounds() {
// This is not a trivial setter, conversion happens in it
statistics.setBounds(12345, -34567, 56789, -98765);
assertEquals(12345, statistics.getLeft());
assertEquals(-34567, statistics.getTop());
assertEquals(56789, statistics.getRight());
assertEquals(-98765, statistics.getBottom());
}
@Test
public void testMerge() {
TripStatistics statistics2 = new TripStatistics();
statistics.setStartTime(1000L); // Resulting start time
@@ -68,38 +63,32 @@ public class TripStatisticsTest extends TestCase {
statistics2.setMaxGrade(35.0); // Resulting max grade
statistics2.setMinGrade(0.0);
// Resulting bounds: -10000, 35000, 30000, -40000
statistics.setBounds(-10000, 20000, 30000, -40000);
statistics2.setBounds(-5000, 35000, 0, 20000);
statistics.merge(statistics2);
assertEquals(1000L, statistics.getStartTime());
assertEquals(4000L, statistics.getStopTime());
assertEquals(2500L, statistics.getTotalTime());
assertEquals(1300L, statistics.getMovingTime());
assertEquals(1100.0, statistics.getTotalDistance());
assertEquals(900.0, statistics.getTotalElevationGain());
assertEquals(statistics.getTotalDistance() / (statistics.getMovingTime() / 1000.0), statistics.getMaxSpeed());
assertEquals(-10000, statistics.getLeft());
assertEquals(30000, statistics.getRight());
assertEquals(35000, statistics.getTop());
assertEquals(-40000, statistics.getBottom());
assertEquals(1200.0, statistics.getMinElevation());
assertEquals(3575.0, statistics.getMaxElevation());
assertEquals(-25.0, statistics.getMinGrade());
assertEquals(35.0, statistics.getMaxGrade());
Assert.assertEquals(1000L, statistics.getStartTime());
Assert.assertEquals(4000L, statistics.getStopTime());
Assert.assertEquals(2500L, statistics.getTotalTime());
Assert.assertEquals(1300L, statistics.getMovingTime());
Assert.assertEquals(1100.0, statistics.getTotalDistance(), 0.001);
Assert.assertEquals(900.0, statistics.getTotalElevationGain(), 0.001);
Assert.assertEquals(statistics.getTotalDistance() / (statistics.getMovingTime() / 1000.0), statistics.getMaxSpeed(), 0.001);
Assert.assertEquals(1200.0, statistics.getMinElevation(), 0.001);
Assert.assertEquals(3575.0, statistics.getMaxElevation(), 0.001);
Assert.assertEquals(-25.0, statistics.getMinGrade(), 0.001);
Assert.assertEquals(35.0, statistics.getMaxGrade(), 0.001);
}
@Test
public void testGetAverageSpeed() {
statistics.setTotalDistance(1000.0);
statistics.setTotalTime(50000); // in milliseconds
assertEquals(20.0, statistics.getAverageSpeed());
Assert.assertEquals(20.0, statistics.getAverageSpeed(), 0.001);
}
@Test
public void testGetAverageMovingSpeed() {
statistics.setTotalDistance(1000.0);
statistics.setMovingTime(20000); // in milliseconds
assertEquals(50.0, statistics.getAverageMovingSpeed());
Assert.assertEquals(50.0, statistics.getAverageMovingSpeed(), 0.001);
}
}
@@ -15,9 +15,10 @@
*/
package de.dennisguse.opentracks.util;
import android.os.Environment;
import junit.framework.TestCase;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.File;
@@ -26,83 +27,83 @@ import java.io.File;
*
* @author Rodrigo Damazio
*/
public class FileUtilsTest extends TestCase {
/**
* Tests {@link FileUtils#getPath(String...)}.
*/
public void testBuildExternalDirectoryPath() {
String expectedName = Environment.getExternalStorageDirectory() + File.separator + FileUtils.SDCARD_TOP_DIR + File.separator + "a" + File.separator + "b" + File.separator + "c";
String dirName = FileUtils.getPath("a", "b", "c");
assertEquals(expectedName, dirName);
}
@RunWith(JUnit4.class)
public class FileUtilsTest {
/**
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the file is new.
*/
@Test
public void testBuildUniqueFileName_new() {
String filename = FileUtils.buildUniqueFileName(new File("/dir"), "Filename", "ext");
assertEquals("Filename.ext", filename);
Assert.assertEquals("Filename.ext", filename);
}
/**
* Tests {@link FileUtils#buildUniqueFileName(File, String, String)} when the file exists already.
*/
@Test
public void testBuildUniqueFileName_exist() {
// Expect "/default.prop" to exist on the phone/emulator
String filename = FileUtils.buildUniqueFileName(new File("/"), "default", "prop");
assertEquals("default(1).prop", filename);
Assert.assertEquals("default(1).prop", filename);
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with special characters.
* Verifies that they are sanitized.
*/
@Test
public void testSanitizeFileName() {
String name = "Swim\10ming-^across:/the/ pacific (ocean).";
String expected = "Swim_ming-^across_the_ pacific (ocean).";
assertEquals(expected, FileUtils.sanitizeFileName(name));
Assert.assertEquals(expected, FileUtils.sanitizeFileName(name));
}
@Test
public void testSanitizeFileNameWithSuffix() {
String name = "FileName.jpeg";
assertEquals(name, FileUtils.sanitizeFileName(name));
Assert.assertEquals(name, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with i18n characters (in Chinese and Russian).
* Verifies that they are allowed.
*/
@Test
public void testSanitizeFileName_i18n() {
String name = "您好-привет";
String expected = "您好-привет";
assertEquals(expected, FileUtils.sanitizeFileName(name));
Assert.assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with special FAT32 characters.
* Verifies that they are allowed.
*/
@Test
public void testSanitizeFileName_special_characters() {
String name = "$%'-_@~`!(){}^#&+,;=[] ";
String expected = "$%'-_@~`!(){}^#&+,;=[] ";
assertEquals(expected, FileUtils.sanitizeFileName(name));
Assert.assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with multiple escaped characters in a row.
* Verifies that they are collapsed into one underscore.
*/
@Test
public void testSanitizeFileName_collapse() {
String name = "hello//there";
String expected = "hello_there";
assertEquals(expected, FileUtils.sanitizeFileName(name));
Assert.assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#truncateFileName(File, String, String)}.
* Verifies the a long file name is truncated.
*/
@Test
public void testTruncateFileName() {
File directory = new File("/dir1/dir2/");
String suffix = ".gpx";
@@ -114,8 +115,8 @@ public class FileUtilsTest extends TestCase {
String truncated = FileUtils.truncateFileName(directory, nameString, suffix);
for (int i = 0; i < truncated.length(); i++) {
assertEquals('a', truncated.charAt(i));
Assert.assertEquals('a', truncated.charAt(i));
}
assertEquals(FileUtils.MAX_FAT32_PATH_LENGTH, new File(directory, truncated + suffix).getPath().length());
Assert.assertEquals(FileUtils.MAX_FAT32_PATH_LENGTH, new File(directory, truncated + suffix).getPath().length());
}
}
@@ -115,27 +115,16 @@ public class StringUtilsTest {
}
/**
* Tests {@link StringUtils#getTime(String)}.
*/
@Test
public void testGetTime() {
assertGetTime("2010-05-04T03:02:01", 2010, 5, 4, 3, 2, 1, 0);
assertGetTime("2010-05-04T03:02:01Z", 2010, 5, 4, 3, 2, 1, 0);
}
/**
* Tests {@link StringUtils#getTime(String)} with fractional seconds.
* Tests {@link StringUtils#parseTime(String)} with fractional seconds.
*/
@Test
public void testGetTime_fractional() {
assertGetTime("2010-05-04T03:02:01.3", 2010, 5, 4, 3, 2, 1, 300);
assertGetTime("2010-05-04T03:02:01.35", 2010, 5, 4, 3, 2, 1, 350);
assertGetTime("2010-05-04T03:02:01.352Z", 2010, 5, 4, 3, 2, 1, 352);
assertGetTime("2010-05-04T03:02:01.3529Z", 2010, 5, 4, 3, 2, 1, 353);
assertGetTime("2010-05-04T03:02:01.3529Z", 2010, 5, 4, 3, 2, 1, 352);
}
/**
* Tests {@link StringUtils#getTime(String)} with time zone.
* Tests {@link StringUtils#parseTime(String)} with time zone.
*/
@Test
public void testGetTime_timezone() {
@@ -149,7 +138,7 @@ public class StringUtilsTest {
}
/**
* Tests {@link StringUtils#getTime(String)} with fractional seconds and time zone.
* Tests {@link StringUtils#parseTime(String)} with fractional seconds and time zone.
*/
@Test
public void testGetTime_fractionalAndTimezone() {
@@ -160,7 +149,7 @@ public class StringUtilsTest {
}
/**
* Asserts the {@link StringUtils#getTime(String)} returns the expected values.
* Asserts the {@link StringUtils#parseTime(String)} returns the expected values.
*
* @param xmlDateTime the xml date time string
* @param year the expected year
@@ -175,7 +164,10 @@ public class StringUtilsTest {
GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
calendar.set(year, month - 1, day, hour, minute, second);
calendar.set(GregorianCalendar.MILLISECOND, millisecond);
Assert.assertEquals(calendar.getTimeInMillis(), StringUtils.getTime(xmlDateTime));
// This comparision tends to be flaky (difference of 1ms)
// Assert.assertEquals(calendar.getTimeInMillis(), StringUtils.parseTime(xmlDateTime));
Assert.assertTrue(calendar.getTimeInMillis() + " vs. " + StringUtils.parseTime(xmlDateTime), Math.abs(calendar.getTimeInMillis() - StringUtils.parseTime(xmlDateTime)) <= 1);
}
/**
+7 -9
View File
@@ -16,22 +16,22 @@ limitations under the License.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.dennisguse.opentracks"
android:installLocation="auto"
android:versionCode="3213"
android:versionName="v3.2.11"> <!-- For FDroid. -->
android:versionCode="3287"
android:versionName="v3.4.0"> <!-- For FDroid. -->
<!-- Permission to initialize services -->
<application
android:name=".Startup"
android:allowBackup="false"
android:hasFragileUserData="true"
android:hardwareAccelerated="true"
android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:icon="${applicationLogo}"
android:label="${applicationName}"
android:theme="@style/ThemeCustom">
<!-- Activities -->
<activity android:name=".AboutActivity" />
<activity android:name=".DeleteActivity" />
<activity android:name=".TrackDeleteActivity" />
<activity
android:name=".HelpActivity"
@@ -101,7 +101,7 @@ limitations under the License.
<activity android:name=".settings.SettingsActivity" />
<provider
android:name=".content.ShareContentProvider"
android:name=".content.provider.ShareContentProvider"
android:authorities="${applicationId}.content"
android:exported="false"
android:grantUriPermissions="true" />
@@ -152,6 +152,4 @@ limitations under the License.
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>
@@ -76,8 +76,8 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
if (stopRecording) {
getTrackRecordingServiceConnection().stopRecording(this, false);
}
Intent intent = IntentUtils.newIntent(this, DeleteActivity.class);
intent.putExtra(DeleteActivity.EXTRA_TRACK_IDS, trackIds);
Intent intent = IntentUtils.newIntent(this, TrackDeleteActivity.class);
intent.putExtra(TrackDeleteActivity.EXTRA_TRACK_IDS, trackIds);
startActivityForResult(intent, DELETE_REQUEST_CODE);
}
@@ -88,7 +88,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
abstract protected TrackRecordingServiceConnection getTrackRecordingServiceConnection();
/**
* Called after {@link DeleteActivity} returns its result.
* Called after {@link TrackDeleteActivity} returns its result.
*/
abstract protected void onDeleted();
}
@@ -19,7 +19,7 @@ package de.dennisguse.opentracks;
import android.content.Context;
import android.os.AsyncTask;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
/**
* Async Task to delete tracks.
@@ -30,7 +30,7 @@ public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private final long[] trackIds;
private final Context context;
private DeleteActivity deleteActivity;
private TrackDeleteActivity deleteActivity;
// true if the AsyncTask has completed
private boolean completed;
@@ -41,7 +41,7 @@ public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
* @param trackIds the track ids to delete. To delete all, set to size 1 with
* trackIds[0] == -1L
*/
public DeleteAsyncTask(DeleteActivity deleteActivity, long[] trackIds) {
public DeleteAsyncTask(TrackDeleteActivity deleteActivity, long[] trackIds) {
this.deleteActivity = deleteActivity;
this.trackIds = trackIds;
context = deleteActivity.getApplicationContext();
@@ -53,7 +53,7 @@ public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
*
* @param deleteActivity the current activity, can be null
*/
public void setActivity(DeleteActivity deleteActivity) {
public void setActivity(TrackDeleteActivity deleteActivity) {
this.deleteActivity = deleteActivity;
if (completed && deleteActivity != null) {
deleteActivity.onAsyncTaskCompleted();
@@ -30,8 +30,8 @@ import androidx.viewpager.widget.ViewPager;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.fragments.MarkerDetailFragment;
@@ -71,12 +71,9 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
if (cursor != null && cursor.moveToFirst()) {
for (int i = 0; i < cursor.getCount(); i++) {
Waypoint currentMarker = contentProviderUtils.createWaypoint(cursor);
if (!currentMarker.isTripStatistics()) {
markerIds.add(currentMarker.getId());
if (currentMarker.getId() == markerId) {
markerIndex = markerIds.size() - 1;
}
markerIds.add(currentMarker.getId());
if (currentMarker.getId() == markerId) {
markerIndex = markerIds.size() - 1;
}
cursor.moveToNext();
@@ -25,8 +25,8 @@ import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
/**
@@ -37,9 +37,9 @@ import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.util.ActivityUtils;
@@ -70,10 +70,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused;
/*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
@@ -99,7 +96,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
// UI elements
private ListView listView;
// Callback when an item is selected in the contextual action mode
private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isSingleSelection = ids.length == 1;
@@ -40,12 +40,12 @@ import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.SearchEngine;
import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
@@ -104,7 +104,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
private boolean recordingTrackPaused;
// Callback when an item is selected in the contextual action mode
private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
@@ -14,7 +14,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
*/
public class Startup extends Application {
private final static String TAG = Startup.class.getSimpleName();
private static final String TAG = Startup.class.getSimpleName();
@Override
public void onCreate() {
@@ -23,8 +23,8 @@ import android.view.View.OnClickListener;
import android.widget.ImageButton;
import android.widget.TextView;
import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -61,7 +61,7 @@ public class TrackController {
public void run() {
if (isResumed && isRecording && !isPaused) {
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(System.currentTimeMillis() - totalTimeTimestamp + totalTime));
handlerUpdateTotalTime.postDelayed(this, UnitConversions.ONE_SECOND);
handlerUpdateTotalTime.postDelayed(this, UnitConversions.ONE_SECOND_MS);
}
}
};
@@ -115,7 +115,7 @@ public class TrackController {
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
if (!isPaused) {
totalTimeTimestamp = System.currentTimeMillis();
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND);
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND_MS);
}
}
}
@@ -142,7 +142,7 @@ public class TrackController {
* Gets the total time for the current recording track.
*/
private long getTotalTime() {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
return trackRecordingService != null ? trackRecordingService.getTotalTime() : 0L;
}
}
@@ -23,14 +23,13 @@ import android.content.Intent;
import android.os.Bundle;
import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.R;
/**
* An activity for delete tracks.
*
* @author Jimmy Shih
*/
public class DeleteActivity extends Activity {
public class TrackDeleteActivity extends Activity {
public static final String EXTRA_TRACK_IDS = "track_ids";
@@ -34,7 +34,6 @@ import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentPagerAdapter;
@@ -42,18 +41,16 @@ import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.fragments.ChartDistanceFragment;
import de.dennisguse.opentracks.fragments.ChartTimeFragment;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.fragments.ChartFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.StatsFragment;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -108,17 +105,12 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
if (hasPhoto && photoUri != null) {
hasPhoto = false;
long markerId = trackRecordingServiceConnection.addMarker(TrackDetailActivity.this, null, null, null, photoUri.toString());
if (markerId != -1L) {
//TODO: Make configurable.
FileUtils.updateMediaScanner(TrackDetailActivity.this, photoUri);
}
}
}
});
}
};
// Note that sharedPreferenceChangeListener cannot be an anonymous inner class. Anonymous inner class will get garbage collected.
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
@@ -200,7 +192,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
trackDataHub = TrackDataHub.newInstance(this);
trackDataHub = new TrackDataHub(this);
FragmentPagerAdapter adapter = new FragmentPagerAdapter(getSupportFragmentManager(), 1) {
@Override
@@ -212,17 +204,15 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
@Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return new StatsFragment();
case 1:
return new ChartTimeFragment();
return ChartFragment.newInstance(false);
case 2:
return new ChartDistanceFragment();
return ChartFragment.newInstance(true);
default: //0
return new StatsFragment();
}
return null;
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
@@ -383,10 +373,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
startActivity(intent);
return true;
case R.id.track_detail_insert_photo:
if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
return false;
}
createWaypointWithPicture();
return true;
case R.id.track_detail_menu_show_on_map:
@@ -30,8 +30,8 @@ import android.widget.Spinner;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -50,11 +50,11 @@ import androidx.loader.content.Loader;
import java.util.Locale;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils;
@@ -127,10 +127,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
private boolean recordingTrackPaused;
/*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner class.
* Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
@@ -196,7 +192,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
return;
}
ITrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
TrackRecordingServiceInterface service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.d(TAG, "service not available to start gps or a new recording");
return;
@@ -409,7 +405,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackRecordingServiceConnection.startAndBind(this);
bindChangedCallback.run();
} else {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
trackRecordingService.stopGps();
}
@@ -13,7 +13,7 @@ import androidx.annotation.Nullable;
*/
public class ContentResolverWrapper implements IContentResolver {
private ContentResolver contentResolver;
private final ContentResolver contentResolver;
public ContentResolverWrapper(ContentResolver contentResolver) {
this.contentResolver = contentResolver;
@@ -122,7 +122,7 @@ public class ChartView extends View {
private boolean reportSpeed = true;
private boolean showPointer = false;
private GestureDetectorCompat detectorScrollFlingTab = new GestureDetectorCompat(getContext(), new GestureDetector.SimpleOnGestureListener() {
private final GestureDetectorCompat detectorScrollFlingTab = new GestureDetectorCompat(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
@@ -176,7 +176,7 @@ public class ChartView extends View {
}
});
private ScaleGestureDetector detectorZoom = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
private final ScaleGestureDetector detectorZoom = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
@@ -18,13 +18,9 @@ package de.dennisguse.opentracks.content;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.ContentObserver;
import android.net.Uri;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* Data source on the phone.
*
@@ -33,47 +29,16 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
class DataSource {
private final ContentResolver contentResolver;
private final SharedPreferences sharedPreferences;
DataSource(Context context) {
contentResolver = context.getContentResolver();
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
}
/**
* Registers a content observer.
*
* @param uri the uri
* @param observer the observer
*/
void registerContentObserver(Uri uri, ContentObserver observer) {
contentResolver.registerContentObserver(uri, false, observer);
}
/**
* Unregisters a content observer.
*
* @param observer the observer
*/
void unregisterContentObserver(ContentObserver observer) {
contentResolver.unregisterContentObserver(observer);
}
/**
* Registers a shared preference change listener.
*
* @param listener the listener
*/
void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
sharedPreferences.registerOnSharedPreferenceChangeListener(listener);
}
/**
* Unregisters a shared preference change listener.
*
* @param listener the listener
*/
void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
}
}
@@ -38,11 +38,4 @@ public interface DataSourceListener {
* Notifies when the track points table is updated.
*/
void notifyTrackPointsTableUpdated();
/**
* Notifies when a preference changes.
*
* @param key the preference key
*/
void notifyPreferenceChanged(String key);
}
@@ -16,8 +16,6 @@
package de.dennisguse.opentracks.content;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.ContentObserver;
import android.os.Handler;
import android.util.Log;
@@ -49,7 +47,6 @@ class DataSourceManager {
private final TracksTableObserver tracksTableObserver;
private final WaypointsTableObserver waypointsTableObserver;
private final TrackPointsTableObserver trackPointsTableObserver;
private final PreferenceListener preferenceListener;
DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) {
this.dataSource = dataSource;
@@ -59,7 +56,6 @@ class DataSourceManager {
tracksTableObserver = new TracksTableObserver();
waypointsTableObserver = new WaypointsTableObserver();
trackPointsTableObserver = new TrackPointsTableObserver();
preferenceListener = new PreferenceListener();
}
/**
@@ -70,10 +66,7 @@ class DataSourceManager {
void updateListeners(EnumSet<TrackDataType> listeners) {
EnumSet<TrackDataType> neededListeners = EnumSet.copyOf(listeners);
/*
* Map SAMPLED_OUT_POINT_UPDATES to POINT_UPDATES since they correspond to
* the same internal listener
*/
// Map SAMPLED_OUT_POINT_UPDATES to POINT_UPDATES since they correspond to the same internal listener
if (neededListeners.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)) {
neededListeners.remove(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE);
neededListeners.add(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE);
@@ -118,15 +111,11 @@ class DataSourceManager {
dataSource.registerContentObserver(WaypointsColumns.CONTENT_URI, waypointsTableObserver);
break;
case SAMPLED_IN_TRACK_POINTS_TABLE:
dataSource.registerContentObserver(
TrackPointsColumns.CONTENT_URI, trackPointsTableObserver);
dataSource.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, trackPointsTableObserver);
break;
case SAMPLED_OUT_TRACK_POINTS_TABLE:
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
break;
case PREFERENCE:
dataSource.registerOnSharedPreferenceChangeListener(preferenceListener);
break;
default:
break;
}
@@ -151,9 +140,6 @@ class DataSourceManager {
case SAMPLED_OUT_TRACK_POINTS_TABLE:
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
break;
case PREFERENCE:
dataSource.unregisterOnSharedPreferenceChangeListener(preferenceListener);
break;
default:
break;
}
@@ -218,17 +204,4 @@ class DataSourceManager {
dataSourceListener.notifyTrackPointsTableUpdated();
}
}
/**
* Listener for preference changes.
*
* @author Jimmy Shih
*/
private class PreferenceListener implements OnSharedPreferenceChangeListener {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
dataSourceListener.notifyPreferenceChanged(key);
}
}
}
@@ -39,7 +39,7 @@ public class DescriptionGenerator {
private static final String TEXT_LINE_BREAK = "\n";
private static final String TEXT_PARAGRAPH_SEPARATOR = "\n\n";
private Context context;
private final Context context;
public DescriptionGenerator(Context context) {
this.context = context;
@@ -72,15 +72,6 @@ public class DescriptionGenerator {
return builder.toString();
}
/**
* Generate a waypoint description from a trip statistics.
*
* @param tripStatistics the trip statistics
*/
public String generateWaypointDescription(TripStatistics tripStatistics) {
return generateTripStatisticsDescription(tripStatistics, false);
}
/**
* Writes a string to a string builder.
*
@@ -1,20 +0,0 @@
package de.dennisguse.opentracks.content;
import android.location.Location;
import android.location.LocationManager;
/**
* Creates a new {@link SensorDataSetLocation}.
* An implementation can create new instances or reuse existing instances for optimization.
*/
public class LocationFactory {
/**
* The default {@link LocationFactory} which creates a location each time.
*/
public static LocationFactory DEFAULT_LOCATION_FACTORY = new LocationFactory();
public Location createLocation() {
return new SensorDataSetLocation(LocationManager.GPS_PROVIDER);
}
}
@@ -1,22 +0,0 @@
package de.dennisguse.opentracks.content;
import android.database.Cursor;
import android.location.Location;
import java.util.Iterator;
/**
* A lightweight wrapper around the original {@link Cursor} with a method to clean up.
*/
public interface LocationIterator extends Iterator<Location>, AutoCloseable {
/**
* Gets the most recently retrieved track point id by {@link #next()}.
*/
long getLocationId();
/**
* Closes the iterator.
*/
void close();
}
@@ -32,6 +32,7 @@ import de.dennisguse.opentracks.content.data.Track;
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.provider.ContentProviderUtils;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -105,7 +106,7 @@ public class SearchEngine {
public static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L;
/**
* Comparador for scored results.
* Comparator for scored results.
*/
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR = new Comparator<ScoredResult>() {
@Override
@@ -247,7 +248,6 @@ public class SearchEngine {
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
TripStatistics statistics = track.getTripStatistics();
score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude());
long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L;
score *= getTimeBoost(query, meanTimestamp);
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks.content;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.location.Location;
import android.os.Handler;
@@ -32,6 +33,9 @@ import java.util.Set;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointFactory;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -40,11 +44,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
*
* @author Rodrigo Damazio
*/
public class TrackDataHub implements DataSourceListener {
public synchronized static TrackDataHub newInstance(Context context) {
return new TrackDataHub(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACK_POINTS);
}
public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSharedPreferenceChangeListener {
/**
* Target number of track points displayed by the map overlay.
@@ -77,24 +77,16 @@ public class TrackDataHub implements DataSourceListener {
private long selectedTrackId;
private long recordingTrackId;
private boolean recordingTrackPaused;
private boolean metricUnits;
private boolean reportSpeed;
private int recordingGpsAccuracy;
private int recordingDistanceInterval;
// Track points sampling state
private int numLoadedPoints;
private long firstSeenLocationId;
private long lastSeenLocationId;
/**
* Constructor.
*
* @param context the context
* @param trackDataManager the track data manager
* @param contentProviderUtils the content tracks provider utils
* @param targetNumPoints the target number of points
*/
public TrackDataHub(Context context) {
this(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACK_POINTS);
}
@VisibleForTesting
private TrackDataHub(Context context, TrackDataManager trackDataManager, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
this.context = context;
@@ -116,7 +108,8 @@ public class TrackDataHub implements DataSourceListener {
dataSource = newDataSource();
dataSourceManager = new DataSourceManager(dataSource, this);
notifyPreferenceChanged(null);
PreferencesUtils.register(context, this);
onSharedPreferenceChanged(null, null);
runInHandlerThread(new Runnable() {
@Override
public void run() {
@@ -133,6 +126,9 @@ public class TrackDataHub implements DataSourceListener {
Log.i(TAG, "TrackDataHub not started, ignoring stop.");
return;
}
PreferencesUtils.unregister(context, this);
started = false;
dataSourceManager.unregisterAllListeners();
@@ -253,7 +249,7 @@ public class TrackDataHub implements DataSourceListener {
}
@Override
public void notifyPreferenceChanged(final String key) {
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key) {
runInHandlerThread(new Runnable() {
@Override
public void run() {
@@ -263,46 +259,6 @@ public class TrackDataHub implements DataSourceListener {
if (PreferencesUtils.isKey(context, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(context);
}
if (PreferencesUtils.isKey(context, R.string.stats_units_key, key)) {
metricUnits = PreferencesUtils.isMetricUnits(context);
if (key != null) {
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onMetricUnitsChanged(metricUnits)) {
loadDataForListener(trackDataListener);
}
}
}
}
if (PreferencesUtils.isKey(context, R.string.stats_rate_key, key)) {
reportSpeed = PreferencesUtils.isReportSpeed(context);
if (key != null) {
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onReportSpeedChanged(reportSpeed)) {
loadDataForListener(trackDataListener);
}
}
}
}
if (PreferencesUtils.isKey(context, R.string.recording_gps_accuracy_key, key)) {
recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
if (key != null) {
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy)) {
loadDataForListener(trackDataListener);
}
}
}
}
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(context);
if (key != null) {
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval)) {
loadDataForListener(trackDataListener);
}
}
}
}
}
});
}
@@ -316,13 +272,6 @@ public class TrackDataHub implements DataSourceListener {
return;
}
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
trackDataListener.onMetricUnitsChanged(metricUnits);
trackDataListener.onReportSpeedChanged(reportSpeed);
trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy);
trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval);
}
notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE));
for (TrackDataListener listener : trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)) {
@@ -343,13 +292,6 @@ public class TrackDataHub implements DataSourceListener {
Set<TrackDataListener> trackDataListeners = Collections.singleton(trackDataListener);
EnumSet<TrackDataType> trackDataTypes = trackDataManager.getTrackDataTypes(trackDataListener);
if (trackDataTypes.contains(TrackDataType.PREFERENCE)) {
trackDataListener.onMetricUnitsChanged(metricUnits);
trackDataListener.onReportSpeedChanged(reportSpeed);
trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy);
trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval);
}
if (trackDataTypes.contains(TrackDataType.TRACKS_TABLE)) {
notifyTracksTableUpdate(trackDataListeners);
}
@@ -450,11 +392,11 @@ public class TrackDataHub implements DataSourceListener {
int samplingFrequency = -1;
boolean includeNextPoint = false;
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
try (TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY)) {
while (locationIterator.hasNext()) {
Location location = locationIterator.next();
long locationId = locationIterator.getLocationId();
long locationId = locationIterator.getTrackPointId();
// Stop if past the last wanted point
if (maxPointId != -1L && locationId > maxPointId) {
@@ -542,32 +484,4 @@ public class TrackDataHub implements DataSourceListener {
public long getSelectedTrackId() {
return selectedTrackId;
}
/**
* Gets the recordingGpsAccuracy.
*/
@VisibleForTesting
int getRecordingGpsAccuracy() {
return recordingGpsAccuracy;
}
/**
* Gets the metricUnits.
*
* @return the metricUnits
*/
@VisibleForTesting
boolean isMetricUnits() {
return metricUnits;
}
/**
* Gets the reportSpeed.
*
* @return the reportSpeed
*/
@VisibleForTesting
boolean isReportSpeed() {
return reportSpeed;
}
}
@@ -77,36 +77,4 @@ public interface TrackDataListener {
* This gets called after every batch of calls to {@link #clearWaypoints()} and {@link #onNewWaypoint(Waypoint)}.
*/
void onNewWaypointsDone();
/**
* Called when the metric units preference value is change.
*
* @param metricUnits true to use metric units, false to use imperial units
* @return true to reload all the data, false otherwise.
*/
boolean onMetricUnitsChanged(boolean metricUnits);
/**
* Called when the report speed preference value is changed.
*
* @param reportSpeed true to report speed, false to report pace
* @return true to reload all the data, false otherwise.
*/
boolean onReportSpeedChanged(boolean reportSpeed);
/**
* Called when the recording gps accuracy preference value is changed.
*
* @param recordingGpsAccuracy the new value
* @return true to reload all the data, false otherwise
*/
boolean onRecordingGpsAccuracy(int recordingGpsAccuracy);
/**
* Called when the recording distance interval preference value is changed.
*
* @param recordingDistanceInterval the new value
* @return true to reload all the data, false otherwise.
*/
boolean onRecordingDistanceIntervalChanged(int recordingDistanceInterval);
}
@@ -26,5 +26,4 @@ public enum TrackDataType {
WAYPOINTS_TABLE, // waypoints table changes
SAMPLED_IN_TRACK_POINTS_TABLE, // sampled-in track points table changes
SAMPLED_OUT_TRACK_POINTS_TABLE, // sampled-out track points table changes
PREFERENCE // preference changes
}
@@ -18,6 +18,8 @@ package de.dennisguse.opentracks.content.data;
import android.location.Location;
import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
@@ -35,8 +37,6 @@ public class Track {
private String name = "";
private String description = "";
private String category = "";
private long startId = -1L;
private long stopId = -1L;
/*
* The number of location points (present even if the points themselves are not loaded)
@@ -84,22 +84,6 @@ public class Track {
this.category = category;
}
public long getStartId() {
return startId;
}
public void setStartId(long startId) {
this.startId = startId;
}
public long getStopId() {
return stopId;
}
public void setStopId(long stopId) {
this.stopId = stopId;
}
public int getNumberOfPoints() {
return numberOfPoints;
}
@@ -124,6 +108,7 @@ public class Track {
this.tripStatistics = tripStatistics;
}
@VisibleForTesting
public void addLocation(Location location) {
locations.add(location);
}
@@ -13,7 +13,7 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.content;
package de.dennisguse.opentracks.content.data;
import android.location.Location;
@@ -24,16 +24,16 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
*
* @author Sandor Dornbush
*/
public class SensorDataSetLocation extends Location {
public class TrackPoint extends Location {
private SensorDataSet sensorDataSet = null;
public SensorDataSetLocation(Location location, SensorDataSet sensorDataSet) {
public TrackPoint(Location location, SensorDataSet sensorDataSet) {
super(location);
this.sensorDataSet = sensorDataSet;
}
public SensorDataSetLocation(String provider) {
public TrackPoint(String provider) {
super(provider);
}
@@ -19,7 +19,7 @@ package de.dennisguse.opentracks.content.data;
import android.net.Uri;
import android.provider.BaseColumns;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
/**
* Constants for the track points table.
@@ -29,15 +29,20 @@ import de.dennisguse.opentracks.content.ContentProviderUtils;
public interface TrackPointsColumns extends BaseColumns {
String TABLE_NAME = "trackpoints";
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/trackpoints");
Uri CONTENT_URI_BY_ID = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
Uri CONTENT_URI_BY_TRACKID = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/trackid");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.trackpoint";
String DEFAULT_SORT_ORDER = "_id";
// Columns
String TRACKID = "trackid";
String LONGITUDE = "longitude";
String LATITUDE = "latitude";
double PAUSE_LATITUDE = 100.0;
double RESUME_LATITUDE = 200.0;
String TIME = "time";
String ALTITUDE = "elevation";
String ACCURACY = "accuracy";
@@ -19,7 +19,7 @@ package de.dennisguse.opentracks.content.data;
import android.net.Uri;
import android.provider.BaseColumns;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
/**
* Constants for the tracks table.
@@ -29,7 +29,7 @@ import de.dennisguse.opentracks.content.ContentProviderUtils;
public interface TracksColumns extends BaseColumns {
String TABLE_NAME = "tracks";
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/tracks");
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.track";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.track";
String DEFAULT_SORT_ORDER = "_id";
@@ -38,22 +38,28 @@ public interface TracksColumns extends BaseColumns {
String NAME = "name"; // track name
String DESCRIPTION = "description"; // track description
String CATEGORY = "category"; // track activity type
String STARTID = "startid"; // first track point id
String STOPID = "stopid"; // last track point id
@Deprecated
String STARTID = "startid"; // first track point id // TODO UNUSED
@Deprecated
String STOPID = "stopid"; // last track point id // TODO UNUSED
String STARTTIME = "starttime"; // track start time
String STOPTIME = "stoptime"; // track stop time
String NUMPOINTS = "numpoints"; // number of track points
String TOTALDISTANCE = "totaldistance"; // total distance
String TOTALTIME = "totaltime"; // total time
String MOVINGTIME = "movingtime"; // moving time
String MINLAT = "minlat"; // minimum latitude
String MAXLAT = "maxlat"; // maximum latitude
String MINLON = "minlon"; // minimum longitude
String MAXLON = "maxlon"; // maximum longitude
String AVGSPEED = "avgspeed"; // average speed
// average moving speed
String AVGMOVINGSPEED = "avgmovingspeed";
@Deprecated
String MINLAT = "minlat"; // minimum latitude // TODO UNUSED
@Deprecated
String MAXLAT = "maxlat"; // maximum latitude // TODO UNUSED
@Deprecated
String MINLON = "minlon"; // minimum longitude //TODO UNUSED
@Deprecated
String MAXLON = "maxlon"; // maximum longitude //TODO UNUSED
String AVGSPEED = "avgspeed"; // average speed
String AVGMOVINGSPEED = "avgmovingspeed"; // average moving speed
String MAXSPEED = "maxspeed"; // maximum speed
String MINELEVATION = "minelevation"; // minimum elevation
String MAXELEVATION = "maxelevation"; // maximum elevation
@@ -19,8 +19,6 @@ package de.dennisguse.opentracks.content.data;
import android.location.Location;
import android.net.Uri;
import de.dennisguse.opentracks.stats.TripStatistics;
/**
* A waypoint.
*
@@ -37,16 +35,13 @@ public final class Waypoint {
private long trackId = -1L;
private double length = 0.0;
private long duration = 0;
private long startId = -1L;
private long stopId = -1L;
private Location location = null;
private TripStatistics tripStatistics = null;
private String photoUrl = "";
public Waypoint() {
}
public Waypoint(String name, String description, String category, String icon, long trackId, double length, long duration, long startId, long stopId, Location location, TripStatistics tripStatistics, String photoUrl) {
public Waypoint(String name, String description, String category, String icon, long trackId, double length, long duration, Location location, String photoUrl) {
this.name = name;
this.description = description;
this.category = category;
@@ -54,10 +49,7 @@ public final class Waypoint {
this.trackId = trackId;
this.length = length;
this.duration = duration;
this.startId = startId;
this.stopId = stopId;
this.location = location;
this.tripStatistics = tripStatistics;
this.photoUrl = photoUrl;
}
@@ -125,22 +117,6 @@ public final class Waypoint {
this.duration = duration;
}
public long getStartId() {
return startId;
}
public void setStartId(long startId) {
this.startId = startId;
}
public long getStopId() {
return stopId;
}
public void setStopId(long stopId) {
this.stopId = stopId;
}
public Location getLocation() {
return location;
}
@@ -149,18 +125,6 @@ public final class Waypoint {
this.location = location;
}
public boolean isTripStatistics() {
return tripStatistics != null;
}
public TripStatistics getTripStatistics() {
return tripStatistics;
}
public void setTripStatistics(TripStatistics tripStatistics) {
this.tripStatistics = tripStatistics;
}
public String getPhotoUrl() {
return photoUrl;
}
@@ -19,7 +19,7 @@ package de.dennisguse.opentracks.content.data;
import android.net.Uri;
import android.provider.BaseColumns;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
/**
* Constants for waypoints table.
@@ -29,7 +29,7 @@ import de.dennisguse.opentracks.content.ContentProviderUtils;
public interface WaypointsColumns extends BaseColumns {
String TABLE_NAME = "waypoints";
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/waypoints");
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.waypoint";
String DEFAULT_SORT_ORDER = "_id";
@@ -40,13 +40,16 @@ public interface WaypointsColumns extends BaseColumns {
String CATEGORY = "category"; // waypoint category
String ICON = "icon"; // waypoint icon
String TRACKID = "trackid"; // track id
@Deprecated //Unused
String TYPE = "type"; // type
@Deprecated
String TYPE = "type"; // type // TODO UNUSED
String LENGTH = "length"; // length of the track (without smoothing)
String DURATION = "duration"; // total duration of the track (not from last waypoint)
String STARTTIME = "starttime"; // start time of the trip statistics
String STARTID = "startid"; // start track point id
String STOPID = "stopid"; // stop track point id
@Deprecated
String STARTID = "startid"; // start track point id // TODO UNUSED
@Deprecated
String STOPID = "stopid"; // stop track point id // TODO UNUSED
String LONGITUDE = "longitude"; // longitude
String LATITUDE = "latitude"; // latitude
@@ -56,17 +59,29 @@ public interface WaypointsColumns extends BaseColumns {
String SPEED = "speed"; // speed
String BEARING = "bearing"; // bearing
@Deprecated
String TOTALDISTANCE = "totaldistance"; // total distance
@Deprecated
String TOTALTIME = "totaltime"; // total time
@Deprecated
String MOVINGTIME = "movingtime"; // moving time
@Deprecated
String AVGSPEED = "avgspeed"; // average speed
String AVGMOVINGSPEED = "avgmovingspeed"; // average moving speed
String MAXSPEED = "maxspeed"; // max speed
String MINELEVATION = "minelevation"; // min elevation
String MAXELEVATION = "maxelevation"; // max elevation
String ELEVATIONGAIN = "elevationgain"; // elevation gain
String MINGRADE = "mingrade"; // min grade
String MAXGRADE = "maxgrade"; // max grade
@Deprecated
String AVGMOVINGSPEED = "avgmovingspeed"; // average moving speed // TODO UNUSED
@Deprecated
String MAXSPEED = "maxspeed"; // max speed // TODO UNUSED
@Deprecated
String MINELEVATION = "minelevation"; // min elevation // TODO UNUSED
@Deprecated
String MAXELEVATION = "maxelevation"; // max elevation // TODO UNUSED
@Deprecated
String ELEVATIONGAIN = "elevationgain"; // elevation gain // TODO UNUSED
@Deprecated
String MINGRADE = "mingrade"; // min grade // TODO UNUSED
@Deprecated
String MAXGRADE = "maxgrade"; // max grade // TODO UNUSED
String PHOTOURL = "photoUrl"; // url for the photo
@@ -0,0 +1,36 @@
package de.dennisguse.opentracks.content.provider;
import android.database.Cursor;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
/**
* A cache of track points indexes.
*/
class CachedTrackPointsIndexes {
final int idIndex;
final int longitudeIndex;
final int latitudeIndex;
final int timeIndex;
final int altitudeIndex;
final int accuracyIndex;
final int speedIndex;
final int bearingIndex;
final int sensorHeartRateIndex;
final int sensorCadenceIndex;
final int sensorPowerIndex;
CachedTrackPointsIndexes(Cursor cursor) {
idIndex = cursor.getColumnIndex(TrackPointsColumns._ID);
longitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE);
latitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE);
timeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.TIME);
altitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE);
accuracyIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ACCURACY);
speedIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED);
bearingIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING);
sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE);
sensorCadenceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_CADENCE);
sensorPowerIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_POWER);
}
}
@@ -14,7 +14,7 @@
* the License.
*/
package de.dennisguse.opentracks.content;
package de.dennisguse.opentracks.content.provider;
import android.content.ContentResolver;
import android.content.ContentValues;
@@ -22,7 +22,7 @@ import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import android.util.Log;
import android.text.TextUtils;
import androidx.annotation.VisibleForTesting;
@@ -30,12 +30,12 @@ import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.android.ContentResolverWrapper;
import de.dennisguse.opentracks.android.IContentResolver;
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.TracksColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
@@ -58,17 +58,18 @@ public class ContentProviderUtils {
/**
* The authority (the first part of the URI) for the app's content provider.
*/
static String AUTHORITY_PACKAGE = BuildConfig.APPLICATION_ID + ".content";
static final String AUTHORITY_PACKAGE = BuildConfig.APPLICATION_ID + ".content";
/**
* The base URI for the app's content provider.
*/
public static String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
public static final String CONTENT_BASE_URI = "content://" + AUTHORITY_PACKAGE;
/**
* Maximum number of waypoints that will be loaded at one time.
*/
public static int MAX_LOADED_WAYPOINTS_POINTS = 10000;
public static final int MAX_LOADED_WAYPOINTS_POINTS = 10000;
public static final String ID_SEPARATOR = ",";
private final IContentResolver contentResolver;
private int defaultCursorBatchSize = 2000;
@@ -108,18 +109,12 @@ public class ContentProviderUtils {
int nameIndex = cursor.getColumnIndexOrThrow(TracksColumns.NAME);
int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION);
int categoryIndex = cursor.getColumnIndexOrThrow(TracksColumns.CATEGORY);
int startIdIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTID);
int stopIdIndex = cursor.getColumnIndexOrThrow(TracksColumns.STOPID);
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
int stopTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STOPTIME);
int numPointsIndex = cursor.getColumnIndexOrThrow(TracksColumns.NUMPOINTS);
int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
int movingTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.MOVINGTIME);
int minLatIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINLAT);
int maxLatIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXLAT);
int minLonIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINLON);
int maxLonIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXLON);
int maxSpeedIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXSPEED);
int minElevationIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINELEVATION);
int maxElevationIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXELEVATION);
@@ -142,12 +137,6 @@ public class ContentProviderUtils {
if (!cursor.isNull(categoryIndex)) {
track.setCategory(cursor.getString(categoryIndex));
}
if (!cursor.isNull(startIdIndex)) {
track.setStartId(cursor.getLong(startIdIndex));
}
if (!cursor.isNull(stopIdIndex)) {
track.setStopId(cursor.getLong(stopIdIndex));
}
if (!cursor.isNull(startTimeIndex)) {
tripStatistics.setStartTime(cursor.getLong(startTimeIndex));
}
@@ -166,13 +155,6 @@ public class ContentProviderUtils {
if (!cursor.isNull(movingTimeIndex)) {
tripStatistics.setMovingTime(cursor.getLong(movingTimeIndex));
}
if (!cursor.isNull(minLatIndex) && !cursor.isNull(maxLatIndex) && !cursor.isNull(minLonIndex) && !cursor.isNull(maxLonIndex)) {
int bottom = cursor.getInt(minLatIndex);
int top = cursor.getInt(maxLatIndex);
int left = cursor.getInt(minLonIndex);
int right = cursor.getInt(maxLonIndex);
tripStatistics.setBounds(left, top, right, bottom);
}
if (!cursor.isNull(maxSpeedIndex)) {
tripStatistics.setMaxSpeed(cursor.getFloat(maxSpeedIndex));
}
@@ -201,13 +183,13 @@ public class ContentProviderUtils {
* Deletes all tracks (including waypoints and track points).
*/
public void deleteAllTracks(Context context) {
contentResolver.delete(TrackPointsColumns.CONTENT_URI, null, null);
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, null, null);
contentResolver.delete(WaypointsColumns.CONTENT_URI, null, null);
// Delete tracks last since it triggers a database vaccum call
contentResolver.delete(TracksColumns.CONTENT_URI, null, null);
File dir = FileUtils.getPhotoDir();
deleteDirectoryRecurse(context, dir);
File dir = FileUtils.getPhotoDir(context);
deleteDirectoryRecurse(dir);
}
/**
@@ -224,23 +206,19 @@ public class ContentProviderUtils {
}
/**
* Deletes track points and waypoints of a track. Assumes
* {@link TracksColumns#STARTID}, {@link TracksColumns#STOPID}, and
* {@link TracksColumns#NUMPOINTS} will be updated by the caller.
* Deletes track points and waypoints of a track.
* Assumes {@link TracksColumns#NUMPOINTS} will be updated by the caller.
*
* @param trackId the track id
*/
private void deleteTrackPointsAndWaypoints(Context context, long trackId) {
Track track = getTrack(trackId);
if (track != null) {
String where = TrackPointsColumns._ID + ">=? AND " + TrackPointsColumns._ID + "<=?";
String[] selectionArgs = new String[]{
Long.toString(track.getStartId()), Long.toString(track.getStopId())};
contentResolver.delete(TrackPointsColumns.CONTENT_URI, where, selectionArgs);
}
String where = TrackPointsColumns.TRACKID + "=?";
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(context, FileUtils.getPhotoDir(trackId));
deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
}
/**
@@ -248,15 +226,12 @@ public class ContentProviderUtils {
*
* @param dir the directory
*/
private void deleteDirectoryRecurse(Context context, File dir) {
if (FileUtils.isDirectory(dir)) {
private void deleteDirectoryRecurse(File dir) {
if (dir != null && dir.exists() && dir.isDirectory()) {
for (File child : dir.listFiles()) {
deleteDirectoryRecurse(context, child);
deleteDirectoryRecurse(child);
}
}
if (dir.exists()) {
dir.delete();
FileUtils.updateMediaScanner(context, Uri.fromFile(dir));
}
}
@@ -265,9 +240,10 @@ public class ContentProviderUtils {
* If no track exists, an empty list is returned.
* NOTE: the returned tracks do not have any track points attached.
*/
@VisibleForTesting
public List<Track> getAllTracks() {
ArrayList<Track> tracks = new ArrayList<>();
try (Cursor cursor = getTrackCursor(null, null, null, TracksColumns._ID)) {
try (Cursor cursor = getTrackCursor(null, null, TracksColumns._ID)) {
if (cursor != null && cursor.moveToFirst()) {
tracks.ensureCapacity(cursor.getCount());
do {
@@ -282,7 +258,7 @@ public class ContentProviderUtils {
* Gets the last track or null.
*/
public Track getLastTrack() {
try (Cursor cursor = getTrackCursor(null, null, null, TracksColumns.STARTTIME + " DESC")) {
try (Cursor cursor = getTrackCursor(null, null, TracksColumns.STARTTIME + " DESC")) {
// Using the same order as shown in the track list
if (cursor != null && cursor.moveToNext()) {
return createTrack(cursor);
@@ -301,7 +277,7 @@ public class ContentProviderUtils {
if (trackId < 0) {
return null;
}
try (Cursor cursor = getTrackCursor(null, TracksColumns._ID + "=?", new String[]{Long.toString(trackId)}, TracksColumns._ID)) {
try (Cursor cursor = getTrackCursor(TracksColumns._ID + "=?", new String[]{Long.toString(trackId)}, TracksColumns._ID)) {
if (cursor != null && cursor.moveToNext()) {
return createTrack(cursor);
}
@@ -318,7 +294,7 @@ public class ContentProviderUtils {
* @param sortOrder the sort order. Can be null
*/
public Cursor getTrackCursor(String selection, String[] selectionArgs, String sortOrder) {
return getTrackCursor(null, selection, selectionArgs, sortOrder);
return contentResolver.query(TracksColumns.CONTENT_URI, null, selection, selectionArgs, sortOrder);
}
/**
@@ -354,18 +330,12 @@ public class ContentProviderUtils {
values.put(TracksColumns.NAME, track.getName());
values.put(TracksColumns.DESCRIPTION, track.getDescription());
values.put(TracksColumns.CATEGORY, track.getCategory());
values.put(TracksColumns.STARTID, track.getStartId());
values.put(TracksColumns.STOPID, track.getStopId());
values.put(TracksColumns.STARTTIME, tripStatistics.getStartTime());
values.put(TracksColumns.STOPTIME, tripStatistics.getStopTime());
values.put(TracksColumns.NUMPOINTS, track.getNumberOfPoints());
values.put(TracksColumns.TOTALDISTANCE, tripStatistics.getTotalDistance());
values.put(TracksColumns.TOTALTIME, tripStatistics.getTotalTime());
values.put(TracksColumns.MOVINGTIME, tripStatistics.getMovingTime());
values.put(TracksColumns.MINLAT, tripStatistics.getBottom());
values.put(TracksColumns.MAXLAT, tripStatistics.getTop());
values.put(TracksColumns.MINLON, tripStatistics.getLeft());
values.put(TracksColumns.MAXLON, tripStatistics.getRight());
values.put(TracksColumns.AVGSPEED, tripStatistics.getAverageSpeed());
values.put(TracksColumns.AVGMOVINGSPEED, tripStatistics.getAverageMovingSpeed());
values.put(TracksColumns.MAXSPEED, tripStatistics.getMaxSpeed());
@@ -379,17 +349,6 @@ public class ContentProviderUtils {
return values;
}
/**
* Gets a track cursor.
*
* @param projection the projection
* @param selection the selection
* @param selectionArgs the selection arguments
* @param sortOrder the sort oder
*/
private Cursor getTrackCursor(String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return contentResolver.query(TracksColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
}
/**
* Creates a waypoint from a cursor.
@@ -403,12 +362,8 @@ public class ContentProviderUtils {
int categoryIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.CATEGORY);
int iconIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.ICON);
int trackIdIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TRACKID);
int typeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TYPE);
int lengthIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.LENGTH);
int durationIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.DURATION);
int startTimeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.STARTTIME);
int startIdIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.STARTID);
int stopIdIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.STOPID);
int longitudeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.LONGITUDE);
int latitudeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.LATITUDE);
int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
@@ -416,15 +371,6 @@ public class ContentProviderUtils {
int accuracyIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.ACCURACY);
int speedIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.SPEED);
int bearingIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.BEARING);
int totalDistanceIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TOTALDISTANCE);
int totalTimeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TOTALTIME);
int movingTimeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MOVINGTIME);
int maxSpeedIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MAXSPEED);
int minElevationIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MINELEVATION);
int maxElevationIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MAXELEVATION);
int elevationGainIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.ELEVATIONGAIN);
int minGradeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MINGRADE);
int maxGradeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.MAXGRADE);
int photoUrlIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.PHOTOURL);
Waypoint waypoint = new Waypoint();
@@ -453,12 +399,6 @@ public class ContentProviderUtils {
if (!cursor.isNull(durationIndex)) {
waypoint.setDuration(cursor.getLong(durationIndex));
}
if (!cursor.isNull(startIdIndex)) {
waypoint.setStartId(cursor.getLong(startIdIndex));
}
if (!cursor.isNull(stopIdIndex)) {
waypoint.setStopId(cursor.getLong(stopIdIndex));
}
Location location = new Location("");
if (!cursor.isNull(longitudeIndex) && !cursor.isNull(latitudeIndex)) {
@@ -482,52 +422,6 @@ public class ContentProviderUtils {
}
waypoint.setLocation(location);
TripStatistics tripStatistics = new TripStatistics();
boolean hasTripStatistics = false;
if (!cursor.isNull(startTimeIndex)) {
tripStatistics.setStartTime(cursor.getLong(startTimeIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(totalDistanceIndex)) {
tripStatistics.setTotalDistance(cursor.getFloat(totalDistanceIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(totalTimeIndex)) {
tripStatistics.setTotalTime(cursor.getLong(totalTimeIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(movingTimeIndex)) {
tripStatistics.setMovingTime(cursor.getLong(movingTimeIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(maxSpeedIndex)) {
tripStatistics.setMaxSpeed(cursor.getFloat(maxSpeedIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(minElevationIndex)) {
tripStatistics.setMinElevation(cursor.getFloat(minElevationIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(maxElevationIndex)) {
tripStatistics.setMaxElevation(cursor.getFloat(maxElevationIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(elevationGainIndex)) {
tripStatistics.setTotalElevationGain(cursor.getFloat(elevationGainIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(minGradeIndex)) {
tripStatistics.setMinGrade(cursor.getFloat(minGradeIndex));
hasTripStatistics = true;
}
if (!cursor.isNull(maxGradeIndex)) {
tripStatistics.setMaxGrade(cursor.getFloat(maxGradeIndex));
hasTripStatistics = true;
}
if (hasTripStatistics) {
waypoint.setTripStatistics(tripStatistics);
}
if (!cursor.isNull(photoUrlIndex)) {
waypoint.setPhotoUrl(cursor.getString(photoUrlIndex));
}
@@ -542,7 +436,7 @@ public class ContentProviderUtils {
* @param waypointId the waypoint id
*/
public void deleteWaypoint(Context context, long waypointId) {
public void deleteWaypoint(long waypointId) {
final Waypoint waypoint = getWaypoint(waypointId);
if (waypoint != null && waypoint.hasPhoto()) {
Uri uri = waypoint.getPhotoURI();
@@ -550,7 +444,6 @@ public class ContentProviderUtils {
if (file.exists()) {
File parent = file.getParentFile();
file.delete();
FileUtils.updateMediaScanner(context, uri);
if (parent.listFiles().length == 0) {
parent.delete();
}
@@ -559,26 +452,6 @@ public class ContentProviderUtils {
contentResolver.delete(WaypointsColumns.CONTENT_URI, WaypointsColumns._ID + "=?", new String[]{Long.toString(waypointId)});
}
/**
* Gets the last waypoint for a type. Returns null if it doesn't exist.
*
* @param trackId the track id
*/
public Waypoint getLastWaypoint(long trackId) {
if (trackId < 0) {
return null;
}
String selection = WaypointsColumns.TRACKID + "=?";
String[] selectionArgs = new String[]{Long.toString(trackId)};
try (Cursor cursor = getWaypointCursor(null, selection, selectionArgs, WaypointsColumns._ID + " DESC", 1)) {
if (cursor != null && cursor.moveToFirst()) {
return createWaypoint(cursor);
}
}
return null;
}
/**
* Gets the next waypoint number for a type.
* Returns -1 if not able to get the next waypoint number.
@@ -728,8 +601,6 @@ public class ContentProviderUtils {
values.put(WaypointsColumns.TRACKID, waypoint.getTrackId());
values.put(WaypointsColumns.LENGTH, waypoint.getLength());
values.put(WaypointsColumns.DURATION, waypoint.getDuration());
values.put(WaypointsColumns.STARTID, waypoint.getStartId());
values.put(WaypointsColumns.STOPID, waypoint.getStopId());
Location location = waypoint.getLocation();
if (location != null) {
@@ -750,22 +621,6 @@ public class ContentProviderUtils {
}
}
TripStatistics tripStatistics = waypoint.getTripStatistics();
if (tripStatistics != null) {
values.put(WaypointsColumns.STARTTIME, tripStatistics.getStartTime());
values.put(WaypointsColumns.TOTALDISTANCE, tripStatistics.getTotalDistance());
values.put(WaypointsColumns.TOTALTIME, tripStatistics.getTotalTime());
values.put(WaypointsColumns.MOVINGTIME, tripStatistics.getMovingTime());
values.put(WaypointsColumns.AVGSPEED, tripStatistics.getAverageSpeed());
values.put(WaypointsColumns.AVGMOVINGSPEED, tripStatistics.getAverageMovingSpeed());
values.put(WaypointsColumns.MAXSPEED, tripStatistics.getMaxSpeed());
values.put(WaypointsColumns.MINELEVATION, tripStatistics.getMinElevation());
values.put(WaypointsColumns.MAXELEVATION, tripStatistics.getMaxElevation());
values.put(WaypointsColumns.ELEVATIONGAIN, tripStatistics.getTotalElevationGain());
values.put(WaypointsColumns.MINGRADE, tripStatistics.getMinGrade());
values.put(WaypointsColumns.MAXGRADE, tripStatistics.getMaxGrade());
}
values.put(WaypointsColumns.PHOTOURL, waypoint.getPhotoUrl());
return values;
}
@@ -806,7 +661,7 @@ public class ContentProviderUtils {
for (int i = 0; i < length; i++) {
values[i] = createContentValues(locations[i], trackId);
}
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI, values);
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
}
/**
@@ -815,7 +670,7 @@ public class ContentProviderUtils {
* @param cursor the cursor pointing to the location
*/
public Location createTrackPoint(Cursor cursor) {
Location location = new SensorDataSetLocation("");
Location location = new TrackPoint("");
fillTrackPoint(cursor, new CachedTrackPointsIndexes(cursor), location);
return location;
}
@@ -826,6 +681,7 @@ public class ContentProviderUtils {
*
* @param trackId the track id
*/
@Deprecated
public long getFirstTrackPointId(long trackId) {
if (trackId < 0) {
return -1L;
@@ -848,6 +704,7 @@ public class ContentProviderUtils {
*
* @param trackId the track id
*/
@Deprecated
public long getLastTrackPointId(long trackId) {
if (trackId < 0) {
return -1L;
@@ -876,8 +733,8 @@ public class ContentProviderUtils {
return -1L;
}
String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID
+ ") from " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID
+ "=? AND " + TrackPointsColumns.TIME + "=?)";
+ ") 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()) {
@@ -893,6 +750,7 @@ public class ContentProviderUtils {
*
* @param trackId the track id
*/
@Deprecated
public Location getLastValidTrackPoint(long trackId) {
if (trackId < 0) {
return null;
@@ -940,166 +798,6 @@ public class ContentProviderUtils {
}
/**
* Creates a new read-only iterator over a given track's points.
* It provides a lightweight way of iterating over long tracks without failing due to the underlying cursor limitations.
* Since it's a read-only iterator, {@link Iterator#remove()} always throws {@link UnsupportedOperationException}.
* Each call to {@link LocationIterator#next()} may advance to the next DB record, and if so, the iterator calls {@link LocationFactory#createLocation()} and populates it with information retrieved from the record.
* When done with iteration, {@link LocationIterator#close()} must be called.
*
* @param trackId the track id
* @param startTrackPointId the starting track point id. -1L to ignore
* @param descending true to sort the result in descending order (latest location first)
* @param locationFactory the location factory
*/
public LocationIterator getTrackPointLocationIterator(final long trackId,
final long startTrackPointId, final boolean descending,
final LocationFactory locationFactory) {
if (locationFactory == null) {
throw new IllegalArgumentException("locationFactory is null");
}
return new LocationIterator() {
private long lastTrackPointId = -1L;
private Cursor cursor = getCursor(startTrackPointId);
private final CachedTrackPointsIndexes
indexes = cursor != null ? new CachedTrackPointsIndexes(cursor)
: null;
/**
* Gets the track point cursor.
*
* @param trackPointId the starting track point id
*/
private Cursor getCursor(long trackPointId) {
return getTrackPointCursor(trackId, trackPointId, defaultCursorBatchSize, descending);
}
/**
* Advances the cursor to the next batch. Returns true if successful.
*/
private boolean advanceCursorToNextBatch() {
long trackPointId = lastTrackPointId == -1L ? -1L
: lastTrackPointId + (descending ? -1 : 1);
Log.d(TAG, "Advancing track point id: " + trackPointId);
cursor.close();
cursor = getCursor(trackPointId);
return cursor != null;
}
@Override
public long getLocationId() {
return lastTrackPointId;
}
@Override
public boolean hasNext() {
if (cursor == null) {
return false;
}
if (cursor.isAfterLast()) {
return false;
}
if (cursor.isLast()) {
if (cursor.getCount() != defaultCursorBatchSize) {
return false;
}
return advanceCursorToNextBatch() && !cursor.isAfterLast();
}
return true;
}
@Override
public Location next() {
if (cursor == null) {
throw new NoSuchElementException();
}
if (!cursor.moveToNext()) {
if (!advanceCursorToNextBatch() || !cursor.moveToNext()) {
throw new NoSuchElementException();
}
}
lastTrackPointId = cursor.getLong(indexes.idIndex);
Location location = locationFactory.createLocation();
fillTrackPoint(cursor, indexes, location);
return location;
}
@Override
public void close() {
if (cursor != null) {
cursor.close();
cursor = null;
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* Inserts a track point.
*
* @param location the location
* @param trackId the track id
* @return the content provider URI of the inserted track point
*/
public Uri insertTrackPoint(Location location, long trackId) {
return contentResolver.insert(TrackPointsColumns.CONTENT_URI, createContentValues(location, trackId));
}
/**
* Creates the {@link ContentValues} for a {@link Location}.
*
* @param location the location
* @param trackId the track id
*/
private ContentValues createContentValues(Location location, long trackId) {
ContentValues values = new ContentValues();
values.put(TrackPointsColumns.TRACKID, trackId);
values.put(TrackPointsColumns.LONGITUDE, (int) (location.getLongitude() * 1E6));
values.put(TrackPointsColumns.LATITUDE, (int) (location.getLatitude() * 1E6));
long time = location.getTime();
// TODO: Check if this is really necessary!
// Hack for Samsung phones that don't properly populate the time field
if (time == 0) {
Log.w(TAG, "location has no timestamp; setting current time");
time = System.currentTimeMillis();
}
values.put(TrackPointsColumns.TIME, time);
if (location.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, location.getAltitude());
}
if (location.hasAccuracy()) {
values.put(TrackPointsColumns.ACCURACY, location.getAccuracy());
}
if (location.hasSpeed()) {
values.put(TrackPointsColumns.SPEED, location.getSpeed());
}
if (location.hasBearing()) {
values.put(TrackPointsColumns.BEARING, location.getBearing());
}
//SensorData
if (location instanceof SensorDataSetLocation) {
SensorDataSetLocation sensorDataSetLocation = (SensorDataSetLocation) location;
SensorDataSet sensorDataSet = sensorDataSetLocation.getSensorDataSet();
if (sensorDataSet != null && sensorDataSet.hasHeartRate()) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, sensorDataSetLocation.getSensorDataSet().getHeartRate());
}
if (sensorDataSet != null && sensorDataSet.hasCadence()) {
values.put(TrackPointsColumns.SENSOR_CADENCE, sensorDataSetLocation.getSensorDataSet().getCadence());
}
if (sensorDataSet != null && sensorDataSet.hasPower()) {
values.put(TrackPointsColumns.SENSOR_POWER, sensorDataSetLocation.getSensorDataSet().getPower());
}
}
return values;
}
/**
* Fills a track point from a cursor.
*
@@ -1107,7 +805,7 @@ public class ContentProviderUtils {
* @param indexes the cached track points indexes
* @param location the track point
*/
private void fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes, Location location) {
static void fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes, Location location) {
location.reset();
if (!cursor.isNull(indexes.longitudeIndex)) {
@@ -1131,8 +829,8 @@ public class ContentProviderUtils {
if (!cursor.isNull(indexes.bearingIndex)) {
location.setBearing(cursor.getFloat(indexes.bearingIndex));
}
if (location instanceof SensorDataSetLocation) {
SensorDataSetLocation sensorDataSetLocation = (SensorDataSetLocation) location;
if (location instanceof TrackPoint) {
TrackPoint sensorDataSetLocation = (TrackPoint) location;
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);
@@ -1142,6 +840,76 @@ public class ContentProviderUtils {
}
}
/**
* Inserts a track point.
*
* @param location the location
* @param trackId the track id
* @return the content provider URI of the inserted track point
*/
public Uri insertTrackPoint(Location location, long trackId) {
return contentResolver.insert(TrackPointsColumns.CONTENT_URI_BY_ID, createContentValues(location, trackId));
}
/**
* Creates the {@link ContentValues} for a {@link Location}.
*
* @param location the location
* @param trackId the track id
*/
private ContentValues createContentValues(Location location, long trackId) {
ContentValues values = new ContentValues();
values.put(TrackPointsColumns.TRACKID, trackId);
values.put(TrackPointsColumns.LONGITUDE, (int) (location.getLongitude() * 1E6));
values.put(TrackPointsColumns.LATITUDE, (int) (location.getLatitude() * 1E6));
values.put(TrackPointsColumns.TIME, location.getTime());
if (location.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, location.getAltitude());
}
if (location.hasAccuracy()) {
values.put(TrackPointsColumns.ACCURACY, location.getAccuracy());
}
if (location.hasSpeed()) {
values.put(TrackPointsColumns.SPEED, location.getSpeed());
}
if (location.hasBearing()) {
values.put(TrackPointsColumns.BEARING, location.getBearing());
}
//SensorData
if (location instanceof TrackPoint) {
TrackPoint sensorDataSetLocation = (TrackPoint) location;
SensorDataSet sensorDataSet = sensorDataSetLocation.getSensorDataSet();
if (sensorDataSet != null && sensorDataSet.hasHeartRate()) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, sensorDataSetLocation.getSensorDataSet().getHeartRate());
}
if (sensorDataSet != null && sensorDataSet.hasCadence()) {
values.put(TrackPointsColumns.SENSOR_CADENCE, sensorDataSetLocation.getSensorDataSet().getCadence());
}
if (sensorDataSet != null && sensorDataSet.hasPower()) {
values.put(TrackPointsColumns.SENSOR_POWER, sensorDataSetLocation.getSensorDataSet().getPower());
}
}
return values;
}
/**
* Creates a new read-only iterator over a given track's points.
* It provides a lightweight way of iterating over long tracks without failing due to the underlying cursor limitations.
* Since it's a read-only iterator, {@link Iterator#remove()} always throws {@link UnsupportedOperationException}.
* Each call to {@link TrackPointIterator#next()} may advance to the next DB record, and if so, the iterator calls {@link TrackPointFactory#createLocation()} and populates it with information retrieved from the record.
* 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 descending true to sort the result in descending order (latest location first)
* @param trackPointFactory the location factory
*/
public TrackPointIterator getTrackPointLocationIterator(final long trackId, final long startTrackPointId, final boolean descending, final TrackPointFactory trackPointFactory) {
return new TrackPointIterator(this, trackId, startTrackPointId, descending, trackPointFactory);
}
private Location findTrackPointBy(String selection, String[] selectionArgs) {
try (Cursor cursor = getTrackPointCursor(null, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToNext()) {
@@ -1160,7 +928,11 @@ public class ContentProviderUtils {
* @param sortOrder the sort order
*/
private Cursor getTrackPointCursor(String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return contentResolver.query(TrackPointsColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
return contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, projection, selection, selectionArgs, sortOrder);
}
int getDefaultCursorBatchSize() {
return defaultCursorBatchSize;
}
/**
@@ -1174,33 +946,24 @@ public class ContentProviderUtils {
}
/**
* A cache of track points indexes.
* Formats an array of IDs as comma separated string value
*
* @param ids array with IDs
* @return comma separated list of ids
*/
private static class CachedTrackPointsIndexes {
final int idIndex;
final int longitudeIndex;
final int latitudeIndex;
final int timeIndex;
final int altitudeIndex;
final int accuracyIndex;
final int speedIndex;
final int bearingIndex;
final int sensorHeartRateIndex;
final int sensorCadenceIndex;
final int sensorPowerIndex;
CachedTrackPointsIndexes(Cursor cursor) {
idIndex = cursor.getColumnIndex(TrackPointsColumns._ID);
longitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE);
latitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE);
timeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.TIME);
altitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE);
accuracyIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ACCURACY);
speedIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED);
bearingIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING);
sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE);
sensorCadenceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_CADENCE);
sensorPowerIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_POWER);
public static String formatIdListForUri(long[] ids) {
StringBuilder idsPathSegment = new StringBuilder();
for (long id : ids) {
if (idsPathSegment.length() > 0) {
idsPathSegment.append(ID_SEPARATOR);
}
idsPathSegment.append(id);
}
return idsPathSegment.toString();
}
}
public static String[] parseTrackIdsFromUri(Uri url) {
return TextUtils.split(url.getLastPathSegment(), ID_SEPARATOR);
}
}
@@ -14,7 +14,7 @@
* the License.
*/
package de.dennisguse.opentracks.content;
package de.dennisguse.opentracks.content.provider;
import android.content.ContentProvider;
import android.content.ContentUris;
@@ -25,7 +25,6 @@ import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
@@ -45,26 +44,25 @@ import de.dennisguse.opentracks.content.data.WaypointsColumns;
*/
public abstract class CustomContentProvider extends ContentProvider {
@VisibleForTesting
static final int DATABASE_VERSION = 23;
@VisibleForTesting
static final String DATABASE_NAME = "database.db";
private static final String TAG = CustomContentProvider.class.getSimpleName();
private static final String SQL_LIST_DELIMITER = ",";
private final UriMatcher uriMatcher;
private SQLiteDatabase db;
public CustomContentProvider() {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.TABLE_NAME, UrlType.TRACKPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.TABLE_NAME + "/#", UrlType.TRACKPOINTS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME, UrlType.TRACKS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/#", UrlType.TRACKS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.TABLE_NAME, UrlType.WAYPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.TABLE_NAME + "/#", UrlType.WAYPOINTS_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath(), UrlType.TRACKPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath() + "/#", UrlType.TRACKPOINTS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.TRACKPOINTS_BY_TRACKID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath(), UrlType.TRACKS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath() + "/*", UrlType.TRACKS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.CONTENT_URI.getPath(), UrlType.WAYPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.CONTENT_URI.getPath() + "/#", UrlType.WAYPOINTS_BY_ID.ordinal());
}
@Override
@@ -80,7 +78,7 @@ public abstract class CustomContentProvider extends ContentProvider {
*/
@VisibleForTesting
boolean onCreate(Context context) {
DatabaseHelper databaseHelper = new DatabaseHelper(context);
CustomSQLiteOpenHelper databaseHelper = new CustomSQLiteOpenHelper(context);
try {
db = databaseHelper.getWritableDatabase();
} catch (SQLiteException e) {
@@ -132,15 +130,16 @@ public abstract class CustomContentProvider extends ContentProvider {
switch (getUrlType(url)) {
case TRACKPOINTS:
return TrackPointsColumns.CONTENT_TYPE;
case TRACKPOINTS_ID:
case TRACKPOINTS_BY_ID:
case TRACKPOINTS_BY_TRACKID:
return TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKS:
return TracksColumns.CONTENT_TYPE;
case TRACKS_ID:
case TRACKS_BY_ID:
return TracksColumns.CONTENT_ITEMTYPE;
case WAYPOINTS:
return WaypointsColumns.CONTENT_TYPE;
case WAYPOINTS_ID:
case WAYPOINTS_BY_ID:
return WaypointsColumns.CONTENT_ITEMTYPE;
default:
throw new IllegalArgumentException("Unknown URL " + url);
@@ -196,25 +195,29 @@ public abstract class CustomContentProvider extends ContentProvider {
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
sortOrder = sort != null ? sort : TrackPointsColumns.DEFAULT_SORT_ORDER;
break;
case TRACKPOINTS_ID:
case TRACKPOINTS_BY_ID:
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
queryBuilder.appendWhere(TrackPointsColumns._ID + "=" + ContentUris.parseId(url));
break;
case TRACKPOINTS_BY_TRACKID:
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
break;
case TRACKS:
queryBuilder.setTables(TracksColumns.TABLE_NAME);
sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER;
break;
case TRACKS_ID:
case TRACKS_BY_ID:
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
break;
case WAYPOINTS:
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
sortOrder = sort != null ? sort : WaypointsColumns.DEFAULT_SORT_ORDER;
break;
case WAYPOINTS_ID:
case WAYPOINTS_BY_ID:
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
queryBuilder.appendWhere(WaypointsColumns._ID + "=" + ContentUris.parseId(url));
break;
default:
throw new IllegalArgumentException("Unknown url " + url);
@@ -226,6 +229,7 @@ public abstract class CustomContentProvider extends ContentProvider {
@Override
public int update(@NonNull Uri url, ContentValues values, String where, String[] selectionArgs) {
// TODO Use SQLiteQueryBuilder
String table;
String whereClause;
switch (getUrlType(url)) {
@@ -233,9 +237,9 @@ public abstract class CustomContentProvider extends ContentProvider {
table = TrackPointsColumns.TABLE_NAME;
whereClause = where;
break;
case TRACKPOINTS_ID:
case TRACKPOINTS_BY_ID:
table = TrackPointsColumns.TABLE_NAME;
whereClause = TrackPointsColumns._ID + "=" + url.getPathSegments().get(1);
whereClause = TrackPointsColumns._ID + "=" + ContentUris.parseId(url);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
}
@@ -244,9 +248,9 @@ public abstract class CustomContentProvider extends ContentProvider {
table = TracksColumns.TABLE_NAME;
whereClause = where;
break;
case TRACKS_ID:
case TRACKS_BY_ID:
table = TracksColumns.TABLE_NAME;
whereClause = TracksColumns._ID + "=" + url.getPathSegments().get(1);
whereClause = TracksColumns._ID + "=" + ContentUris.parseId(url);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
}
@@ -255,9 +259,9 @@ public abstract class CustomContentProvider extends ContentProvider {
table = WaypointsColumns.TABLE_NAME;
whereClause = where;
break;
case WAYPOINTS_ID:
case WAYPOINTS_BY_ID:
table = WaypointsColumns.TABLE_NAME;
whereClause = WaypointsColumns._ID + "=" + url.getPathSegments().get(1);
whereClause = WaypointsColumns._ID + "=" + ContentUris.parseId(url);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
}
@@ -282,8 +286,15 @@ public abstract class CustomContentProvider extends ContentProvider {
*
* @param url the url
*/
@NonNull
private UrlType getUrlType(Uri url) {
return UrlType.values()[uriMatcher.match(url)];
UrlType[] urlTypes = UrlType.values();
int matchIndex = uriMatcher.match(url);
if (0 <= matchIndex && matchIndex < urlTypes.length) {
return urlTypes[matchIndex];
}
throw new IllegalArgumentException("Unknown URL " + url);
}
/**
@@ -321,7 +332,7 @@ public abstract class CustomContentProvider extends ContentProvider {
}
long rowId = db.insert(TrackPointsColumns.TABLE_NAME, TrackPointsColumns._ID, values);
if (rowId >= 0) {
return ContentUris.appendId(TrackPointsColumns.CONTENT_URI.buildUpon(), rowId).build();
return ContentUris.appendId(TrackPointsColumns.CONTENT_URI_BY_ID.buildUpon(), rowId).build();
}
throw new SQLiteException("Failed to insert a track point " + url);
}
@@ -334,8 +345,7 @@ public abstract class CustomContentProvider extends ContentProvider {
*/
private Uri insertTrack(Uri url, ContentValues contentValues) {
boolean hasStartTime = contentValues.containsKey(TracksColumns.STARTTIME);
boolean hasStartId = contentValues.containsKey(TracksColumns.STARTID);
if (!hasStartTime || !hasStartId) {
if (!hasStartTime) {
throw new IllegalArgumentException("Both start time and start id values are required.");
}
long rowId = db.insert(TracksColumns.TABLE_NAME, TracksColumns._ID, contentValues);
@@ -366,33 +376,12 @@ public abstract class CustomContentProvider extends ContentProvider {
*/
@VisibleForTesting
enum UrlType {
TRACKPOINTS, TRACKPOINTS_ID, TRACKS, TRACKS_ID, WAYPOINTS, WAYPOINTS_ID
TRACKPOINTS,
TRACKPOINTS_BY_ID,
TRACKPOINTS_BY_TRACKID,
TRACKS,
TRACKS_BY_ID,
WAYPOINTS,
WAYPOINTS_BY_ID
}
/**
* Database helper for creating and upgrading the database.
*/
@VisibleForTesting
static class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context) {
this(context, DATABASE_NAME);
}
@VisibleForTesting
public DatabaseHelper(Context context, String databaseName) {
super(context, databaseName, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(TrackPointsColumns.CREATE_TABLE);
db.execSQL(TracksColumns.CREATE_TABLE);
db.execSQL(WaypointsColumns.CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
}
}
@@ -0,0 +1,44 @@
package de.dennisguse.opentracks.content.provider;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
/**
* Database helper for creating and upgrading the database.
*/
@VisibleForTesting
public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
@VisibleForTesting
static final int DATABASE_VERSION = 23;
@VisibleForTesting
static final String DATABASE_NAME = "database.db";
public CustomSQLiteOpenHelper(Context context) {
this(context, DATABASE_NAME);
}
@VisibleForTesting
public CustomSQLiteOpenHelper(Context context, String databaseName) {
super(context, databaseName, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(TrackPointsColumns.CREATE_TABLE);
db.execSQL(TracksColumns.CREATE_TABLE);
db.execSQL(WaypointsColumns.CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.content;
package de.dennisguse.opentracks.content.provider;
import android.content.Context;
import android.content.UriMatcher;
@@ -81,7 +81,7 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
}
builder.deleteCharAt(builder.lastIndexOf(TRACKID_DELIMITER));
Uri uri = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TracksColumns.TABLE_NAME + "/" + trackFileFormat.getName() + "/" + builder + "." + trackFileFormat.getExtension());
Uri uri = Uri.parse(TracksColumns.CONTENT_URI + "/" + trackFileFormat.getName() + "/" + builder + "." + trackFileFormat.getExtension());
String mime = getTypeMime(uri);
Log.d(TAG, "Created uri " + uri.toString() + " with MIME " + mime);
@@ -0,0 +1,21 @@
package de.dennisguse.opentracks.content.provider;
import android.location.LocationManager;
import de.dennisguse.opentracks.content.data.TrackPoint;
/**
* Creates a new {@link TrackPoint}.
* An implementation can create new instances or reuse existing instances for optimization.
*/
public class TrackPointFactory {
/**
* The default {@link TrackPointFactory} which creates a location each time.
*/
public static final TrackPointFactory DEFAULT_LOCATION_FACTORY = new TrackPointFactory();
public TrackPoint createLocation() {
return new TrackPoint(LocationManager.GPS_PROVIDER);
}
}
@@ -0,0 +1,111 @@
package de.dennisguse.opentracks.content.provider;
import android.database.Cursor;
import android.util.Log;
import java.util.Iterator;
import java.util.NoSuchElementException;
import de.dennisguse.opentracks.content.data.TrackPoint;
/**
* A lightweight wrapper around the original {@link Cursor} with a method to clean up.
*/
public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
private static final String TAG = TrackPointIterator.class.getSimpleName();
private final ContentProviderUtils contentProviderUtils;
private final long trackId;
private final boolean descending;
private final TrackPointFactory trackPointFactory;
private final CachedTrackPointsIndexes indexes;
private long lastTrackPointId = -1L;
private Cursor cursor;
public TrackPointIterator(ContentProviderUtils contentProviderUtils, long trackId, long startTrackPointId, boolean descending, TrackPointFactory trackPointFactory) {
if (trackPointFactory == null) {
throw new IllegalArgumentException("trackPointFactory is null");
}
this.contentProviderUtils = contentProviderUtils;
this.trackId = trackId;
this.descending = descending;
this.trackPointFactory = trackPointFactory;
cursor = getCursor(startTrackPointId);
indexes = cursor != null ? new CachedTrackPointsIndexes(cursor)
: null;
}
/**
* Gets the track point cursor.
*
* @param trackPointId the starting track point id
*/
private Cursor getCursor(long trackPointId) {
return contentProviderUtils.getTrackPointCursor(trackId, trackPointId, contentProviderUtils.getDefaultCursorBatchSize(), descending);
}
/**
* Advances the cursor to the next batch. Returns true if successful.
*/
private boolean advanceCursorToNextBatch() {
long trackPointId = lastTrackPointId == -1L ? -1L : lastTrackPointId + (descending ? -1 : 1);
Log.d(TAG, "Advancing track point id: " + trackPointId);
cursor.close();
cursor = getCursor(trackPointId);
return cursor != null;
}
public long getTrackPointId() {
return lastTrackPointId;
}
@Override
public boolean hasNext() {
if (cursor == null) {
return false;
}
if (cursor.isAfterLast()) {
return false;
}
if (cursor.isLast()) {
if (cursor.getCount() != contentProviderUtils.getDefaultCursorBatchSize()) {
return false;
}
return advanceCursorToNextBatch() && !cursor.isAfterLast();
}
return true;
}
@Override
public TrackPoint next() {
if (cursor == null) {
throw new NoSuchElementException();
}
if (!cursor.moveToNext()) {
if (!advanceCursorToNextBatch() || !cursor.moveToNext()) {
throw new NoSuchElementException();
}
}
lastTrackPointId = cursor.getLong(indexes.idIndex);
TrackPoint trackPoint = trackPointFactory.createLocation();
ContentProviderUtils.fillTrackPoint(cursor, indexes, trackPoint);
return trackPoint;
}
@Override
public void close() {
if (cursor != null) {
cursor.close();
cursor = null;
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
@@ -20,7 +20,6 @@ public enum SensorState {
case CONNECTED:
return c.getString(R.string.sensor_state_connected);
case DISCONNECTING:
return c.getString(R.string.sensor_state_disconnected);
case DISCONNECTED:
return c.getString(R.string.sensor_state_disconnected);
default:
@@ -1,10 +0,0 @@
package de.dennisguse.opentracks.fragments;
public class ChartDistanceFragment extends ChartFragment {
public static final String CHART_FRAGMENT_TAG = ChartDistanceFragment.class.getSimpleName();
public ChartDistanceFragment() {
super(true);
}
}
@@ -16,6 +16,7 @@
package de.dennisguse.opentracks.fragments;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -24,7 +25,6 @@ import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
@@ -36,11 +36,11 @@ import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.TrackDataType;
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.sensor.SensorDataSet;
import de.dennisguse.opentracks.stats.TripStatistics;
@@ -55,9 +55,18 @@ import de.dennisguse.opentracks.util.UnitConversions;
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public abstract class ChartFragment extends Fragment implements TrackDataListener {
public class ChartFragment extends Fragment implements TrackDataListener {
private static final String STATE_CHART_VIEW_BY_DISTANCE_KEY = "chartViewByDistance";
private static final String KEY_CHART_VIEW_BY_DISTANCE_KEY = "chartViewByDistance";
public static Fragment newInstance(boolean chartByDistance) {
Bundle bundle = new Bundle();
bundle.putBoolean(KEY_CHART_VIEW_BY_DISTANCE_KEY, chartByDistance);
ChartFragment chartFragment = new ChartFragment();
chartFragment.setArguments(bundle);
return chartFragment;
}
private final List<double[]> pendingPoints = new ArrayList<>();
@@ -73,11 +82,56 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
// Modes of operation
private boolean chartByDistance;
private boolean[] chartShow = new boolean[]{true, true, true, true, true, true};
private final boolean[] chartShow = new boolean[]{true, true, true, true, true, true};
// UI elements
private ChartView chartView;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (!isResumed()) {
return;
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
boolean metric = PreferencesUtils.isMetricUnits(getContext());
if (metricUnits != metric) {
metricUnits = metric;
chartView.setMetricUnits(metricUnits);
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
chartView.requestLayout();
}
}
});
}
}
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
boolean speed = PreferencesUtils.isReportSpeed(getContext());
if (reportSpeed != speed) {
reportSpeed = speed;
chartView.setReportSpeed(reportSpeed);
setSeriesEnabled(ChartView.SPEED_SERIES, reportSpeed);
setSeriesEnabled(ChartView.PACE_SERIES, !reportSpeed);
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
chartView.requestLayout();
}
}
});
}
}
if (PreferencesUtils.isKey(getContext(), R.string.recording_distance_interval_key, key)) {
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(getContext());
}
}
};
/**
* A runnable that will set the orange pointer as appropriate and redraw.
*/
@@ -93,14 +147,13 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
}
};
public ChartFragment(boolean chartByDistance) {
this.chartByDistance = chartByDistance;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
chartByDistance = getArguments().getBoolean(KEY_CHART_VIEW_BY_DISTANCE_KEY, chartByDistance);
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceIntervalDefault(getContext());
// Create a chartView here to store data thus won't need to reload all the data on every onStart or onResume.
@@ -124,6 +177,8 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
public void onResume() {
super.onResume();
resumeTrackDataHub();
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
checkChartSettings();
getActivity().runOnUiThread(updateChart);
}
@@ -132,6 +187,7 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
public void onPause() {
super.onPause();
pauseTrackDataHub();
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
}
@Override
@@ -141,20 +197,6 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
layout.removeView(chartView);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(STATE_CHART_VIEW_BY_DISTANCE_KEY, chartByDistance);
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (savedInstanceState != null) {
chartByDistance = savedInstanceState.getBoolean(STATE_CHART_VIEW_BY_DISTANCE_KEY);
}
}
@Override
public void onTrackUpdated(Track track) {
if (isResumed()) {
@@ -229,68 +271,6 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
}
}
@Override
public boolean onMetricUnitsChanged(boolean metric) {
if (isResumed()) {
if (metricUnits == metric) {
return false;
}
metricUnits = metric;
chartView.setMetricUnits(metricUnits);
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
chartView.requestLayout();
}
}
});
return true;
}
return false;
}
@Override
public boolean onReportSpeedChanged(boolean speed) {
if (isResumed()) {
if (reportSpeed == speed) {
return false;
}
reportSpeed = speed;
chartView.setReportSpeed(reportSpeed);
setSeriesEnabled(ChartView.SPEED_SERIES, reportSpeed);
setSeriesEnabled(ChartView.PACE_SERIES, !reportSpeed);
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
chartView.requestLayout();
}
}
});
return true;
}
return false;
}
@Override
public boolean onRecordingGpsAccuracy(int minRequiredAccuracy) {
// We don't care.
return false;
}
@Override
public boolean onRecordingDistanceIntervalChanged(int value) {
if (isResumed()) {
if (recordingDistanceInterval == value) {
return false;
}
recordingDistanceInterval = value;
return true;
}
return false;
}
/**
* Checks the chart settings.
*/
@@ -334,7 +314,7 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
TrackDataType.WAYPOINTS_TABLE, TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE, TrackDataType.PREFERENCE));
TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
}
/**
@@ -424,8 +404,8 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
double heartRate = Double.NaN;
double cadence = Double.NaN;
double power = Double.NaN;
if (location instanceof SensorDataSetLocation && ((SensorDataSetLocation) location).getSensorDataSet() != null) {
SensorDataSet sensorDataSet = ((SensorDataSetLocation) location).getSensorDataSet();
if (location instanceof TrackPoint && ((TrackPoint) location).getSensorDataSet() != null) {
SensorDataSet sensorDataSet = ((TrackPoint) location).getSensorDataSet();
if (sensorDataSet.hasHeartRate()) {
heartRate = sensorDataSet.getHeartRate();
}
@@ -1,10 +0,0 @@
package de.dennisguse.opentracks.fragments;
public class ChartTimeFragment extends ChartFragment {
public static final String CHART_FRAGMENT_TAG = ChartTimeFragment.class.getSimpleName();
public ChartTimeFragment() {
super(false);
}
}
@@ -39,7 +39,7 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
return TrackIconUtils.getAllIconValues().indexOf(iconValue);
}
private String preselectedCategory;
private final String preselectedCategory;
private ChooseActivityTypeCaller chooseActivityTypeCaller;
@@ -71,7 +71,6 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
int position = getPosition(getContext(), preselectedCategory);
if (position != -1) {
imageAdapter.setSelected(position);
imageAdapter.notifyDataSetChanged();
}
gridView.setOnItemClickListener(this);
@@ -56,12 +56,9 @@ public class ChooseActivityTypeImageAdapter extends BaseAdapter {
return 0;
}
public int getSelected() {
return selected;
}
public void setSelected(int position) {
selected = position;
notifyDataSetChanged();
}
@Override
@@ -36,12 +36,9 @@ import de.dennisguse.opentracks.util.DialogUtils;
public class ConfirmDeleteDialogFragment extends DialogFragment {
private static final String CONFIRM_DELETE_DIALOG_TAG = "confirmDeleteDialog";
private ConfirmDeleteCaller caller;
private long[] trackIds;
private static final String KEY_TRACK_IDS = "trackIds";
public ConfirmDeleteDialogFragment(long[] trackIds) {
this.trackIds = trackIds;
}
private ConfirmDeleteCaller caller;
/**
* Create a new instance.
@@ -49,7 +46,12 @@ public class ConfirmDeleteDialogFragment extends DialogFragment {
* @param trackIds list of track ids to delete. To delete all, set to size 1 with trackIds[0] == -1L
*/
public static void showDialog(FragmentManager fragmentManager, long[] trackIds) {
new ConfirmDeleteDialogFragment(trackIds).show(fragmentManager, CONFIRM_DELETE_DIALOG_TAG);
Bundle bundle = new Bundle();
bundle.putLongArray(KEY_TRACK_IDS, trackIds);
ConfirmDeleteDialogFragment deleteTrackDialogFragment = new ConfirmDeleteDialogFragment();
deleteTrackDialogFragment.setArguments(bundle);
deleteTrackDialogFragment.show(fragmentManager, CONFIRM_DELETE_DIALOG_TAG);
}
@Override
@@ -65,6 +67,8 @@ public class ConfirmDeleteDialogFragment extends DialogFragment {
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
final long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS);
int titleId;
int messageId;
titleId = trackIds.length > 1 ? R.string.generic_delete_selected_confirm_title : R.string.track_delete_one_confirm_title;
@@ -27,7 +27,7 @@ import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.util.DialogUtils;
/**
@@ -38,16 +38,17 @@ import de.dennisguse.opentracks.util.DialogUtils;
public class DeleteMarkerDialogFragment extends DialogFragment {
private static final String DELETE_MARKER_DIALOG_TAG = "deleteMarkerDialog";
private long[] markerIds;
private static final String KEY_MARKER_IDS = "markerIds";
private DeleteMarkerCaller caller;
public DeleteMarkerDialogFragment(long[] markerIds) {
this.markerIds = markerIds;
}
public static void showDialog(FragmentManager fragmentManager, long[] markerIds) {
new DeleteMarkerDialogFragment(markerIds).show(fragmentManager, DELETE_MARKER_DIALOG_TAG);
Bundle bundle = new Bundle();
bundle.putLongArray(KEY_MARKER_IDS, markerIds);
DeleteMarkerDialogFragment deleteMarkerDialogFragment = new DeleteMarkerDialogFragment();
deleteMarkerDialogFragment.setArguments(bundle);
deleteMarkerDialogFragment.show(fragmentManager, DELETE_MARKER_DIALOG_TAG);
}
@Override
@@ -63,6 +64,8 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
final long[] markerIds = getArguments().getLongArray(KEY_MARKER_IDS);
final FragmentActivity fragmentActivity = getActivity();
int titleId;
int messageId;
@@ -82,7 +85,7 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
public void run() {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(fragmentActivity);
for (long markerId : markerIds) {
contentProviderUtils.deleteWaypoint(fragmentActivity, markerId);
contentProviderUtils.deleteWaypoint(markerId);
}
caller.onDeleteMarkerDone();
}
@@ -31,7 +31,6 @@ import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.FileUtils;
/**
* A DialogFragment to select a file type, gpx, kml, etc.
@@ -41,18 +40,19 @@ import de.dennisguse.opentracks.util.FileUtils;
public class FileTypeDialogFragment extends DialogFragment {
private static final String FILE_TYPE_DIALOG_TAG = "fileType";
private int titleId;
private static final String KEY_TITLE_ID = "titleId";
private static final String KEY_OPTION_ID = "optionId";
private FileTypeCaller caller;
private int optionId;
public FileTypeDialogFragment(int titleId, int optionId) {
this.titleId = titleId;
this.optionId = optionId;
}
public static void showDialog(FragmentManager fragmentManager, int titleId, int optionId) {
new FileTypeDialogFragment(titleId, optionId).show(fragmentManager, FILE_TYPE_DIALOG_TAG);
Bundle bundle = new Bundle();
bundle.putInt(KEY_TITLE_ID, titleId);
bundle.putInt(KEY_OPTION_ID, optionId);
FileTypeDialogFragment fileTypeDialogFragment = new FileTypeDialogFragment();
fileTypeDialogFragment.setArguments(bundle);
fileTypeDialogFragment.show(fragmentManager, FILE_TYPE_DIALOG_TAG);
}
@Override
@@ -68,12 +68,17 @@ public class FileTypeDialogFragment extends DialogFragment {
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
final TrackFileFormat[] trackFileFormats = {TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES, TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA, TrackFileFormat.GPX};
int optionId = getArguments().getInt(KEY_OPTION_ID);
final int titleId = getArguments().getInt(KEY_TITLE_ID);
final TrackFileFormat[] trackFileFormats = {TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES, TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA, TrackFileFormat.GPX};
String[] choices = new String[trackFileFormats.length];
for (int i = 0; i < choices.length; i++) {
TrackFileFormat trackFileFormat = trackFileFormats[i];
String trackFileFormatUpperCase = trackFileFormat.getExtension().toUpperCase(Locale.US); //ASCII upper case
choices[i] = getString(optionId, trackFileFormatUpperCase, FileUtils.getPathDisplayName(trackFileFormat.getExtension()));
int photoMessageId = trackFileFormat.includesPhotos() ? R.string.export_with_photos : R.string.export_without_photos;
String trackFileFormatDisplayName = String.format("%s (%s)", trackFileFormatUpperCase, getString(photoMessageId));
choices[i] = getString(optionId, trackFileFormatDisplayName);
}
return new AlertDialog.Builder(getActivity())
.setNegativeButton(R.string.generic_cancel, new OnClickListener() {
@@ -40,8 +40,8 @@ import androidx.fragment.app.FragmentActivity;
import de.dennisguse.opentracks.MarkerEditActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
@@ -57,14 +57,16 @@ public class MarkerDetailFragment extends Fragment {
private static final String TAG = MarkerDetailFragment.class.getSimpleName();
private static final String KEY_MARKER_ID = "markerId";
private static final long HIDE_TEXT_DELAY = 4 * UnitConversions.ONE_SECOND;
private static final long HIDE_TEXT_DELAY = 4 * UnitConversions.ONE_SECOND_MS;
private ContentProviderUtils contentProviderUtils;
private Handler handler;
private ImageView photoView;
private ImageView textGradient;
private LinearLayout waypointInfo;
private Waypoint waypoint;
private Runnable hideText = new Runnable() {
private final Runnable hideText = new Runnable() {
@Override
public void run() {
Animation animation = AnimationUtils.loadAnimation(getActivity(), R.anim.fadeout);
@@ -219,7 +221,7 @@ public class MarkerDetailFragment extends Fragment {
photoView.setImageResource(MarkerUtils.ICON_ID);
}
setName(R.id.marker_detail_waypoint_name, hasPhoto);
setName(hasPhoto);
TextView category = getView().findViewById(R.id.marker_detail_waypoint_category);
ListItemUtils.setTextView(getActivity(), category, StringUtils.getCategory(waypoint.getCategory()), hasPhoto);
@@ -227,16 +229,16 @@ public class MarkerDetailFragment extends Fragment {
TextView description = getView().findViewById(R.id.marker_detail_waypoint_description);
ListItemUtils.setTextView(getActivity(), description, waypoint.getDescription(), hasPhoto);
setLocation(R.id.marker_detail_waypoint_location, hasPhoto);
setLocation(hasPhoto);
}
private void setName(int resId, boolean addShadow) {
TextView textView = getView().findViewById(resId);
private void setName(boolean addShadow) {
TextView textView = getView().findViewById(R.id.marker_detail_waypoint_name);
ListItemUtils.setTextView(getActivity(), textView, waypoint.getName(), addShadow);
}
private void setLocation(int resId, boolean addShadow) {
TextView textView = getView().findViewById(resId);
private void setLocation(boolean addShadow) {
TextView textView = getView().findViewById(R.id.marker_detail_waypoint_location);
Location location = waypoint.getLocation();
String value;
if (location == null) {
@@ -16,6 +16,7 @@
package de.dennisguse.opentracks.fragments;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
@@ -43,8 +44,8 @@ import de.dennisguse.opentracks.content.TrackDataType;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
@@ -61,9 +62,9 @@ import de.dennisguse.opentracks.util.UnitConversions;
*/
public class StatsFragment extends Fragment implements TrackDataListener {
public static final String STATS_FRAGMENT_TAG = StatsFragment.class.getSimpleName();
private static final String STATS_FRAGMENT_TAG = StatsFragment.class.getSimpleName();
private static final long UI_UPDATE_INTERVAL = UnitConversions.ONE_SECOND;
private static final long UI_UPDATE_INTERVAL = UnitConversions.ONE_SECOND_MS;
private TrackDataHub trackDataHub;
private Handler handlerUpdateUI;
@@ -75,8 +76,30 @@ public class StatsFragment extends Fragment implements TrackDataListener {
private TripStatistics lastTripStatistics = null;
private String category = "";
@Deprecated //TODO This should be handled somewhere else; not in the UI.
private int recordingGpsAccuracy;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
updateUI();
}
}
});
}
}
if (PreferencesUtils.isKey(getContext(), R.string.recording_track_id_key, key)) {
recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(getContext());
}
}
};
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.stats, container, false);
@@ -221,6 +244,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
public void onResume() {
super.onResume();
resumeTrackDataHub();
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null);
trackRecordingServiceConnection.startConnection(getContext());
@@ -232,6 +256,8 @@ public class StatsFragment extends Fragment implements TrackDataListener {
public void onPause() {
super.onPause();
pauseTrackDataHub();
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
handlerUpdateUI.removeCallbacks(updateUIeachSecond);
}
@@ -370,48 +396,6 @@ public class StatsFragment extends Fragment implements TrackDataListener {
// We don't care.
}
@Override
public boolean onMetricUnitsChanged(final boolean metric) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
updateUI();
}
}
});
}
return true;
}
@Override
public boolean onReportSpeedChanged(final boolean speed) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
updateUI();
}
}
});
}
return true;
}
@Override
public boolean onRecordingGpsAccuracy(int newValue) {
recordingGpsAccuracy = newValue;
return false;
}
@Override
public boolean onRecordingDistanceIntervalChanged(int minRecordingDistance) {
// We don't care.
return false;
}
/**
* Resumes the trackDataHub.
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
@@ -419,8 +403,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE,
TrackDataType.PREFERENCE));
TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
}
/**
@@ -458,7 +441,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
* Tries to fetch most recent {@link SensorDataSet} {@link de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager}.
*/
private void updateSensorDataUI() {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
SensorDataSet sensorDataSet = null;
if (trackRecordingService == null) {
@@ -5,8 +5,8 @@ import android.content.Context;
import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.exporter.FileTrackExporter;
import de.dennisguse.opentracks.io.file.exporter.GpxTrackWriter;
import de.dennisguse.opentracks.io.file.exporter.KmlTrackWriter;
@@ -67,7 +67,7 @@ public enum TrackFileFormat {
},
KMZ_ONLY_TRACK {
private final static boolean exportPhotos = false;
private static final boolean exportPhotos = false;
@Override
public TrackWriter newTrackWriter(Context context, boolean multiple) {
@@ -86,10 +86,15 @@ public enum TrackFileFormat {
public String getExtension() {
return "kmz";
}
@Override
public boolean includesPhotos() {
return exportPhotos;
}
},
KMZ_WITH_TRACKDETAIL {
private final static boolean exportPhotos = false;
private static final boolean exportPhotos = false;
@Override
public TrackWriter newTrackWriter(Context context, boolean multiple) {
@@ -108,10 +113,16 @@ public enum TrackFileFormat {
public String getExtension() {
return "kmz";
}
@Override
public boolean includesPhotos() {
return exportPhotos;
}
},
KMZ_WITH_TRACKDETAIL_AND_SENSORDATA {
private final static boolean exportPhotos = false;
private static final boolean exportPhotos = false;
@Override
public TrackWriter newTrackWriter(Context context, boolean multiple) {
@@ -130,10 +141,17 @@ public enum TrackFileFormat {
public String getExtension() {
return "kmz";
}
@Override
public boolean includesPhotos() {
return exportPhotos;
}
},
KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES {
private final static boolean exportPhotos = true;
private static final boolean exportPhotos = true;
@Override
public TrackWriter newTrackWriter(Context context, boolean multiple) {
@@ -152,6 +170,12 @@ public enum TrackFileFormat {
public String getExtension() {
return "kmz";
}
@Override
public boolean includesPhotos() {
return exportPhotos;
}
},
GPX {
@Override
@@ -205,6 +229,13 @@ public enum TrackFileFormat {
*/
public abstract String getExtension();
/**
* Returns whether the format supports photos.
*/
public boolean includesPhotos() {
return false;
}
/**
* Returns the name of for each format.
*/
@@ -16,21 +16,20 @@
package de.dennisguse.opentracks.io.file.exporter;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
import java.io.File;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.fragments.FileTypeDialogFragment;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
@@ -39,7 +38,6 @@ import de.dennisguse.opentracks.util.FileUtils;
/**
* An activity for saving tracks to the external storage.
* If saving a specific track, option to save it to a temp directory and play the track afterward.
*
* @author Rodrigo Damazio
*/
@@ -53,34 +51,34 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
private String directoryDisplayName;
private ExportAsyncTask exportAsyncTask;
private ProgressDialog progressDialog;
private int processedTrackCount;
private int totalTrackCount;
private Uri directoryUri;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
} else {
fileTypeDialogStart();
}
startActivityForResult(intent, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
} else {
if (resultCode == Activity.RESULT_OK) {
directoryUri = resultData.getData();
fileTypeDialogStart();
} else {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void fileTypeDialogStart() {
@@ -89,21 +87,14 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
@Override
public void onFileTypeDone(TrackFileFormat trackFileFormat) {
if (!FileUtils.isExternalStorageWriteable()) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
return;
DocumentFile pickedDirectory = DocumentFile.fromTreeUri(this, directoryUri);
DocumentFile exportDirectory = pickedDirectory.findFile(FileUtils.EXPORT_DIR);
if (exportDirectory == null) {
exportDirectory = pickedDirectory.createDirectory(FileUtils.EXPORT_DIR);
}
File directory = new File(FileUtils.getPath(trackFileFormat.getExtension()));
if (!FileUtils.ensureDirectoryExists(directory)) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
return;
}
directoryDisplayName = FileUtils.getPathDisplayName(trackFileFormat.getExtension());
exportAsyncTask = new ExportAsyncTask(this, trackFileFormat, directory);
directoryDisplayName = FileUtils.getPath(exportDirectory);
exportAsyncTask = new ExportAsyncTask(this, trackFileFormat, exportDirectory);
exportAsyncTask.execute();
}
@@ -141,8 +132,7 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
} 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_external_storage_error, processedTrackCount, totalTracks, directoryDisplayName);
}
AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(true)
.setIcon(iconId).setMessage(message)
@@ -22,17 +22,16 @@ import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import androidx.documentfile.provider.DocumentFile;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
/**
@@ -45,16 +44,16 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ExportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat;
private final File directory;
private final DocumentFile directory;
private final Context context;
private final ContentProviderUtils contentProviderUtils;
private ExportActivity exportActivity;
private WakeLock wakeLock;
private boolean completed;
private boolean completed = false;
private int processedTrackCount;
private int totalTrackCount;
private int processedTrackCount = 0;
private int totalTrackCount = 0;
/**
* Creates an AsyncTask.
@@ -63,16 +62,12 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
* @param trackFileFormat the track file format
* @param directory the directory to write the file
*/
public ExportAsyncTask(ExportActivity exportActivity, TrackFileFormat trackFileFormat, File directory) {
public ExportAsyncTask(ExportActivity exportActivity, TrackFileFormat trackFileFormat, DocumentFile directory) {
this.exportActivity = exportActivity;
this.trackFileFormat = trackFileFormat;
this.directory = directory;
context = exportActivity.getApplicationContext();
contentProviderUtils = new ContentProviderUtils(context);
completed = false;
processedTrackCount = 0;
totalTrackCount = 0;
}
/**
@@ -98,17 +93,12 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
protected Boolean doInBackground(Void... params) {
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
boolean isRecording = PreferencesUtils.isRecording(exportActivity);
boolean isPaused = PreferencesUtils.isRecordingTrackPaused(exportActivity);
// Get the wake lock if not recording or paused
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(exportActivity, wakeLock);
}
return saveAllTracks();
wakeLock = SystemUtils.acquireWakeLock(exportActivity, wakeLock);
return exportAllTracks();
} finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
}
}
@@ -136,15 +126,11 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
/**
* Saves tracks to one file (uses first track to determine the filename).
* Export the track to one file.
*
* @param tracks the tracks
* @param track the track
*/
private Boolean saveTracks(Track[] tracks) {
if (tracks.length == 0) {
return false;
}
private Boolean exportTrack(Track track) {
TrackExporterListener trackExporterListener = new TrackExporterListener() {
@Override
public void onProgressUpdate(int number, int max) {
@@ -155,14 +141,18 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
};
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context, tracks, trackExporterListener);
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context, new Track[]{track}, trackExporterListener);
Track track = tracks[0];
String fileName = FileUtils.buildUniqueFileName(directory, track.getName(), trackFileFormat.getExtension());
File file = new File(directory, fileName);
String fileName = track.getId() + "." + trackFileFormat.getExtension();
try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
if (trackExporter.writeTrack(context, fileOutputStream)) {
// Overwrite a file if it exists; DocumentFile.createFile() creates a new file appending a suffix if the displayname already exists.
DocumentFile file = directory.findFile(fileName);
if (file == null) {
file = directory.createFile(trackFileFormat.getMimeType(), fileName);
}
try (OutputStream outputStream = context.getContentResolver().openOutputStream(file.getUri())) {
if (trackExporter.writeTrack(context, outputStream)) {
return true;
} else {
if (!file.delete()) {
@@ -183,7 +173,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
/**
* Saves all the tracks.
*/
private Boolean saveAllTracks() {
private Boolean exportAllTracks() {
try (Cursor cursor = contentProviderUtils.getTrackCursor(null, null, TracksColumns._ID)) {
if (cursor == null) {
return false;
@@ -195,7 +185,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
cursor.moveToPosition(i);
Track track = contentProviderUtils.createTrack(cursor);
if (track != null && saveTracks(new Track[]{track})) {
if (track != null && exportTrack(track)) {
processedTrackCount++;
}
publishProgress(i + 1, totalTrackCount);
@@ -25,12 +25,12 @@ import androidx.annotation.NonNull;
import java.io.OutputStream;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.LocationFactory;
import de.dennisguse.opentracks.content.LocationIterator;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
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.provider.TrackPointFactory;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.util.LocationUtils;
/**
@@ -108,9 +108,7 @@ public class FileTrackExporter implements TrackExporter {
hasWaypoints = true;
}
Waypoint waypoint = contentProviderUtils.createWaypoint(cursor);
if (!waypoint.isTripStatistics()) {
trackWriter.writeWaypoint(waypoint);
}
trackWriter.writeWaypoint(waypoint);
cursor.moveToNext();
}
@@ -128,10 +126,10 @@ public class FileTrackExporter implements TrackExporter {
boolean wroteTrack = false;
boolean wroteSegment = false;
boolean isLastLocationValid = false;
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
TrackWriterTrackPointFactory trackPointFactory = new TrackWriterTrackPointFactory();
int locationNumber = 0;
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, locationFactory)) {
try (TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, trackPointFactory)) {
while (locationIterator.hasNext()) {
if (Thread.interrupted()) {
@@ -146,7 +144,7 @@ public class FileTrackExporter implements TrackExporter {
boolean isSegmentValid = isLocationValid && isLastLocationValid;
if (!wroteTrack && isSegmentValid) {
// Found the first two consecutive locations that are valid
trackWriter.writeBeginTrack(track, locationFactory.lastLocation);
trackWriter.writeBeginTrack(track, trackPointFactory.lastLocation);
wroteTrack = true;
}
@@ -157,7 +155,7 @@ public class FileTrackExporter implements TrackExporter {
wroteSegment = true;
// Write the previous location, which we had previously skipped
trackWriter.writeLocation(locationFactory.lastLocation);
trackWriter.writeLocation(trackPointFactory.lastLocation);
}
// Write the current location
@@ -171,7 +169,7 @@ public class FileTrackExporter implements TrackExporter {
wroteSegment = false;
}
}
locationFactory.swapLocations();
trackPointFactory.swapLocations();
isLastLocationValid = isLocationValid;
}
@@ -209,20 +207,20 @@ public class FileTrackExporter implements TrackExporter {
*
* @author Jimmy Shih
*/
private class TrackWriterLocationFactory extends LocationFactory {
Location currentLocation;
Location lastLocation;
private class TrackWriterTrackPointFactory extends TrackPointFactory {
TrackPoint currentLocation;
TrackPoint lastLocation;
@Override
public Location createLocation() {
public TrackPoint createLocation() {
if (currentLocation == null) {
currentLocation = new SensorDataSetLocation("");
currentLocation = new TrackPoint("");
}
return currentLocation;
}
void swapLocations() {
Location tempLocation = lastLocation;
TrackPoint tempLocation = lastLocation;
lastLocation = currentLocation;
currentLocation = tempLocation;
if (currentLocation != null) {
@@ -26,11 +26,11 @@ import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.DescriptionGenerator;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
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.StringUtils;
@@ -65,9 +65,9 @@ public class KmlTrackWriter implements TrackWriter {
private final ContentProviderUtils contentProviderUtils;
private PrintWriter printWriter;
private List<Float> powerList = new ArrayList<>();
private List<Float> cadenceList = new ArrayList<>();
private List<Float> heartRateList = new ArrayList<>();
private final List<Float> powerList = new ArrayList<>();
private final List<Float> cadenceList = new ArrayList<>();
private final List<Float> heartRateList = new ArrayList<>();
private Location startLocation;
@@ -164,7 +164,7 @@ public class KmlTrackWriter implements TrackWriter {
if (printWriter != null && exportTrackDetail) {
if (waypoint.hasPhoto() && exportPhotos) {
float heading = getHeading(waypoint.getTrackId(), waypoint.getLocation());
writePhotoOverlay(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), WAYPOINT_STYLE, waypoint.getLocation(), waypoint.getPhotoUrl(), heading);
writePhotoOverlay(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), waypoint.getLocation(), waypoint.getPhotoUrl(), heading);
} else {
writePlacemark(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), WAYPOINT_STYLE, waypoint.getLocation());
}
@@ -264,8 +264,8 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.println("<gx:coord>" + getCoordinates(location, " ") + "</gx:coord>");
if (exportSensorData && location instanceof SensorDataSetLocation) {
SensorDataSet sensorDataSet = ((SensorDataSetLocation) location).getSensorDataSet();
if (exportSensorData && location instanceof TrackPoint) {
SensorDataSet sensorDataSet = ((TrackPoint) location).getSensorDataSet();
if (sensorDataSet != null) {
if (sensorDataSet.hasHeartRate()) {
heartRateList.add(sensorDataSet.getHeartRate());
@@ -321,16 +321,14 @@ public class KmlTrackWriter implements TrackWriter {
/**
* Writes a photo overlay.
*
* @param name the name
* @param name the name
* @param category the category
* @param description the description
* @param styleName the style name
* @param location the location
* @param photoUrl the photo url
* @param heading the heading
*/
private void writePhotoOverlay(String name, String category, String description, String styleName, Location location, String photoUrl, float heading) {
private void writePhotoOverlay(String name, String category, String description, Location location, String photoUrl, float heading) {
if (location != null && exportTrackDetail) {
printWriter.println("<PhotoOverlay>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
@@ -343,7 +341,7 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.print("<tilt>90</tilt>");
printWriter.println("</Camera>");
printWriter.println("<TimeStamp><when>" + getTime(location) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
printWriter.println("<styleUrl>#" + KmlTrackWriter.WAYPOINT_STYLE + "</styleUrl>");
writeCategory(category);
if (exportPhotos) {
@@ -31,9 +31,9 @@ import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.util.FileUtils;
/**
@@ -52,7 +52,7 @@ public class KmzTrackExporter implements TrackExporter {
private final TrackExporter fileTrackExporter;
private final Track[] tracks;
private boolean exportPhotos;
private final boolean exportPhotos;
/**
* Constructor.
@@ -38,13 +38,12 @@ import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.LocationFactory;
import de.dennisguse.opentracks.content.LocationIterator;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointFactory;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.stats.TripStatisticsUpdater;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.LocationUtils;
@@ -161,9 +160,10 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
Waypoint waypoint = null;
Location location = null;
TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
@Deprecated // TODO Should not be necessary anymore?
TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
try (TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY)) {
while (true) {
if (waypoint == null) {
@@ -200,15 +200,13 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
// Valid location
if (location.getLatitude() == waypoint.getLocation().getLatitude() && location.getLongitude() == waypoint.getLocation().getLongitude()) {
TripStatistics tripStatistics = null;
String waypointDescription = waypoint.getDescription();
String icon = context.getString(R.string.marker_waypoint_icon_url);
double length = trackTripStatisticstrackUpdater.getTripStatistics().getTotalDistance();
long duration = trackTripStatisticstrackUpdater.getTripStatistics().getTotalTime();
// Insert waypoint
Waypoint newWaypoint = new Waypoint(waypoint.getName(), waypointDescription, waypoint.getCategory(), icon, track.getId(), length, duration, -1L, -1L, location, tripStatistics, waypoint.getPhotoUrl());
Waypoint newWaypoint = new Waypoint(waypoint.getName(), waypointDescription, waypoint.getCategory(), icon, track.getId(), length, duration, location, waypoint.getPhotoUrl());
contentProviderUtils.insertWaypoint(newWaypoint);
}
@@ -276,7 +274,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
//If not the first segment, add a pause separator if there is at least one location in the last segment.
if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment != null) {
insertLocation(createLocation(TrackRecordingService.PAUSE_LATITUDE, 0.0, 0.0, trackData.lastLocationInCurrentSegment.getTime()));
insertLocation(createLocation(TrackPointsColumns.PAUSE_LATITUDE, 0.0, 0.0, trackData.lastLocationInCurrentSegment.getTime()));
}
trackData.lastLocationInCurrentSegment = null;
}
@@ -346,8 +344,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment == null) {
// If not the first segment, add a resume separator before adding the first location.
insertLocation(
createLocation(TrackRecordingService.RESUME_LATITUDE, 0.0, 0.0, location.getTime()));
insertLocation(createLocation(TrackPointsColumns.RESUME_LATITUDE, 0.0, 0.0, location.getTime()));
}
trackData.lastLocationInCurrentSegment = location;
return location;
@@ -360,11 +357,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
*/
protected void insertTrackPoint(Location location) {
insertLocation(location);
if (trackData.track.getStartId() == -1L) {
// Flush the location to set the track start id and the track end id
flushLocations(trackData);
}
}
/**
@@ -385,7 +377,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (importTrackId == -1L) {
return null;
}
File dir = FileUtils.getPhotoDir(importTrackId);
File dir = FileUtils.getPhotoDir(context, importTrackId);
File file = new File(dir, fileName);
return Uri.fromFile(file).toString();
}
@@ -419,7 +411,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
timeValue = trackData.importTime;
} else {
try {
timeValue = StringUtils.getTime(time);
timeValue = StringUtils.parseTime(time);
} catch (IllegalArgumentException e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
}
@@ -482,10 +474,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
}
contentProviderUtils.bulkInsertTrackPoint(data.bufferedLocations, data.numBufferedLocations, data.track.getId());
data.numBufferedLocations = 0;
if (data.track.getStartId() == -1L) {
data.track.setStartId(contentProviderUtils.getFirstTrackPointId(data.track.getId()));
}
data.track.setStopId(contentProviderUtils.getLastTrackPointId(data.track.getId()));
}
/**
@@ -505,7 +493,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
// TODO Why private inner class?
private class TrackData {
// The current track
Track track = new Track();
final Track track = new Track();
// The number of segments processed for the current track
int numberOfSegments = 0;
@@ -520,10 +508,10 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
TripStatisticsUpdater tripStatisticsUpdater;
// The import time of the track.
long importTime = System.currentTimeMillis();
final long importTime = System.currentTimeMillis();
// The buffered locations
Location[] bufferedLocations = new Location[MAX_BUFFERED_LOCATIONS];
final Location[] bufferedLocations = new Location[MAX_BUFFERED_LOCATIONS];
// The number of buffered locations
int numBufferedLocations = 0;
@@ -24,7 +24,7 @@ import androidx.annotation.VisibleForTesting;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
/**
* Imports a GPX file.
@@ -16,21 +16,20 @@
package de.dennisguse.opentracks.io.file.importer;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
import java.io.File;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.fragments.FileTypeDialogFragment;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
@@ -38,8 +37,7 @@ import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.util.FileUtils;
/**
* An activity to import files from the external storage. Optionally to import
* one specific file.
* An activity to import files from the external storage.
*
* @author Rodrigo Damazio
*/
@@ -50,70 +48,53 @@ public class ImportActivity extends FragmentActivity implements FileTypeDialogFr
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
private String directoryDisplayName;
private ImportAsyncTask importAsyncTask;
private ProgressDialog progressDialog;
// the path on the external storage to import
private String directoryDisplayName;
private int importedTrackCount;
private int totalTrackCount;
private Uri directoryUri;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
} else {
fileTypeDialogStart();
}
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(intent, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
Toast.makeText(this, R.string.external_storage_not_readable, Toast.LENGTH_LONG).show();
} else {
if (resultCode == Activity.RESULT_OK) {
directoryUri = resultData.getData();
fileTypeDialogStart();
} else {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void fileTypeDialogStart() {
FileTypeDialogFragment.showDialog(getSupportFragmentManager(), R.string.import_selection_title, R.string.import_selection_option);
}
@Override
public void onFileTypeDone(TrackFileFormat trackFileFormat) {
if (!FileUtils.isExternalStorageAvailable()) {
Toast.makeText(this, R.string.external_storage_not_available, Toast.LENGTH_LONG).show();
finish();
return;
}
DocumentFile pickedDirectory = DocumentFile.fromTreeUri(this, directoryUri);
directoryDisplayName = FileUtils.getPathDisplayName(trackFileFormat.getExtension());
String directoryPath = FileUtils.getPath(trackFileFormat.getExtension());
if (!FileUtils.isDirectory(new File(directoryPath))) {
Toast.makeText(this, getString(R.string.import_no_directory, directoryDisplayName), Toast.LENGTH_LONG)
.show();
finish();
return;
}
directoryDisplayName = FileUtils.getPath(pickedDirectory);
//TODO (still needed?): getLastNonConfiguration instance returned ExportAsyncTask before
// Object retained = getLastNonConfigurationInstance();
// if (retained instanceof ImportAsyncTask) {
// importAsyncTask = (ImportAsyncTask) retained;
// importAsyncTask.setActivity(this);
// } else {
importAsyncTask = new ImportAsyncTask(this, trackFileFormat, directoryPath);
importAsyncTask = new ImportAsyncTask(this, trackFileFormat, pickedDirectory);
importAsyncTask.execute();
// }
}
@Override
@@ -154,8 +135,7 @@ public class ImportActivity extends FragmentActivity implements FileTypeDialogFr
} else {
iconId = R.drawable.ic_dialog_error_24dp;
titleId = R.string.generic_error_title;
message = getString(
R.string.import_error, importedTrackCount, totalFiles, directoryDisplayName);
message = getString(R.string.import_error, importedTrackCount, totalFiles, directoryDisplayName);
}
return new AlertDialog.Builder(this).setCancelable(true).setIcon(iconId)
.setMessage(message).setOnCancelListener(new DialogInterface.OnCancelListener() {
@@ -22,14 +22,15 @@ import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import androidx.documentfile.provider.DocumentFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -44,7 +45,7 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ImportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat;
private final String path;
private final DocumentFile directory;
// TODO Can be removed, or?
private Context context;
// TODO Use weak reference
@@ -66,12 +67,12 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
*
* @param importActivity the activity currently associated with this AsyncTask
* @param trackFileFormat the track file format
* @param path path to import GPX files
* @param directory path to import GPX files
*/
public ImportAsyncTask(ImportActivity importActivity, TrackFileFormat trackFileFormat, String path) {
public ImportAsyncTask(ImportActivity importActivity, TrackFileFormat trackFileFormat, DocumentFile directory) {
this.importActivity = importActivity;
this.trackFileFormat = trackFileFormat;
this.path = path;
this.directory = directory;
context = importActivity.getApplicationContext();
completed = false;
@@ -99,39 +100,26 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); // TODO Should be set to previous level at the end of this method.
// Get the wake lock if not recording or paused
boolean isRecording = PreferencesUtils.isRecording(importActivity);
boolean isPaused = PreferencesUtils.isRecordingTrackPaused(importActivity);
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
}
/**
* 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<>();
List<File> files = getFiles();
totalTrackCount = files.size();
if (totalTrackCount == 0) {
return true;
}
for (int i = 0; i < totalTrackCount; i++) {
if (isCancelled()) {
// If cancelled, return true to show the number of files imported
return true;
for (DocumentFile candidate : file.listFiles()) {
if (!candidate.isDirectory()) {
String extension = FileUtils.getExtension(candidate.getName());
if (trackFileFormat.getExtension().equals(extension)) {
files.add(candidate);
}
if (importFile(files.get(i))) {
importTrackCount++;
}
publishProgress(i + 1, totalTrackCount);
}
return true;
} finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
} else {
files.addAll(getFiles(candidate, trackFileFormat));
}
}
return files;
}
@Override
@@ -159,16 +147,49 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); // TODO Should be set to previous level at the end of this method.
// Get the wake lock if not recording or paused
boolean isRecording = PreferencesUtils.isRecording(importActivity);
boolean isPaused = PreferencesUtils.isRecordingTrackPaused(importActivity);
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
}
List<DocumentFile> files = getFiles(directory, trackFileFormat);
totalTrackCount = files.size();
for (int i = 0; i < totalTrackCount; i++) {
if (isCancelled()) {
// If cancelled, return true to show the number of files imported
return true;
}
if (importFile(files.get(i))) {
importTrackCount++;
}
publishProgress(i + 1, totalTrackCount);
}
return true;
} finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
}
}
/**
* Imports a file.
*
* @param file the file
*/
private boolean importFile(final File file) {
private boolean importFile(final DocumentFile file) {
TrackImporter trackImporter;
if (trackFileFormat == TrackFileFormat.GPX) {
trackImporter = new GpxFileTrackImporter(importActivity);
} else { //KML or KMZ
} else {
//KML or KMZ
String extension = FileUtils.getExtension(file.getName());
if (TrackFileFormat.KML_ONLY_TRACK.getExtension().equals(extension)) {
trackImporter = new KmlFileTrackImporter(importActivity, -1L);
@@ -181,37 +202,12 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
}
try (FileInputStream fileInputStream = new FileInputStream(file)) {
trackId = trackImporter.importFile(fileInputStream);
try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) {
trackId = trackImporter.importFile(inputStream);
return trackId != -1L;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
return false;
}
}
/**
* 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 List<File> getFiles() {
List<File> files = new ArrayList<>();
File file = new File(path);
File[] candidates = file.listFiles();
if (candidates == null) {
return files;
}
for (File candidate : candidates) {
if (!FileUtils.isDirectory(candidate)) {
String extension = FileUtils.getExtension(candidate.getName());
if (trackFileFormat.getExtension().equals(extension)) {
files.add(candidate);
}
}
}
return files;
}
}
@@ -28,8 +28,8 @@ import org.xml.sax.SAXException;
import java.util.ArrayList;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
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;
@@ -248,7 +248,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
if (!hasSensorData) {
insertTrackPoint(location);
} else {
SensorDataSetLocation sensorDataSetLocation = new SensorDataSetLocation(location, new SensorDataSet(heartrate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, location.getTime()));
TrackPoint sensorDataSetLocation = new TrackPoint(location, new SensorDataSet(heartrate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, location.getTime()));
insertTrackPoint(sensorDataSetLocation);
}
}
@@ -17,7 +17,6 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import java.io.ByteArrayInputStream;
@@ -29,7 +28,7 @@ import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -68,7 +67,7 @@ public class KmzTrackImporter implements TrackImporter {
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
if (Thread.interrupted()) {
Log.d(TAG, "Thread interrupted");
cleanImport(trackId);
cleanImport(context, trackId);
return -1L;
}
String fileName = zipEntry.getName();
@@ -76,7 +75,7 @@ public class KmzTrackImporter implements TrackImporter {
trackId = parseKml(zipInputStream);
if (trackId == -1L) {
Log.d(TAG, "Unable to parse kml in kmz");
cleanImport(trackId);
cleanImport(context, trackId);
return -1L;
}
} else {
@@ -90,7 +89,7 @@ public class KmzTrackImporter implements TrackImporter {
return trackId;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
cleanImport(trackId);
cleanImport(context, trackId);
return -1L;
}
}
@@ -100,20 +99,19 @@ public class KmzTrackImporter implements TrackImporter {
*
* @param trackId the trackId
*/
private void cleanImport(long trackId) {
private void cleanImport(Context context, long trackId) {
if (PreferencesUtils.isRecording(trackId)) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
contentProviderUtils.deleteTrack(context, trackId);
}
if (importTrackId != -1L) {
File dir = FileUtils.getPhotoDir(importTrackId);
if (FileUtils.isDirectory(dir)) {
File dir = FileUtils.getPhotoDir(context, importTrackId);
if (dir.exists() && dir.isDirectory()) {
for (File file : dir.listFiles()) {
file.delete();
}
dir.delete();
FileUtils.updateMediaScanner(context, Uri.fromFile(dir));
}
}
}
@@ -159,8 +157,7 @@ public class KmzTrackImporter implements TrackImporter {
return;
}
File dir = FileUtils.getPhotoDir(importTrackId);
FileUtils.ensureDirectoryExists(dir);
File dir = FileUtils.getPhotoDir(context, importTrackId);
File file = new File(dir, fileName);
try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
@@ -169,8 +166,6 @@ public class KmzTrackImporter implements TrackImporter {
while ((count = zipInputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, count);
}
FileUtils.updateMediaScanner(context, Uri.fromFile(file));
}
}
}
@@ -1,67 +0,0 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.services;
import android.annotation.SuppressLint;
import android.content.Context;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Handler;
import android.os.Looper;
/**
* Handles connection to {@link LocationManager}.
*
* @author Jimmy Shih
*/
public class LocationManagerConnector {
private final Handler handler;
private final LocationManager locationManager;
public LocationManagerConnector(Context context, Looper looper) {
this.handler = new Handler(looper);
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
}
/**
* Requests location updates.
*
* @param minTime the minimal time
* @param minDistance the minimal distance
* @param locationListener the location listener
*/
public void requestLocationUpdates(final long minTime, final float minDistance, final LocationListener locationListener) {
handler.post(new Runnable() {
@Override
@SuppressLint("MissingPermission")
public void run() {
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, minTime, minDistance, locationListener);
}
});
}
/**
* Removes location updates.
*
* @param locationListener the location listener
*/
public void removeLocationUpdates(final LocationListener locationListener) {
locationManager.removeUpdates(locationListener);
}
}
@@ -18,6 +18,7 @@ package de.dennisguse.opentracks.services;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -28,12 +29,10 @@ import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.TaskStackBuilder;
import java.util.concurrent.ExecutorService;
@@ -42,13 +41,14 @@ import java.util.concurrent.Executors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.CustomContentProvider;
import de.dennisguse.opentracks.content.LocationFactory;
import de.dennisguse.opentracks.content.LocationIterator;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
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;
import de.dennisguse.opentracks.content.provider.TrackPointFactory;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
@@ -71,35 +71,26 @@ import de.dennisguse.opentracks.util.UnitConversions;
*/
public class TrackRecordingService extends Service {
private static final int NOTIFICATION_ID = 123;
public static final double PAUSE_LATITUDE = 100.0;
public static final double RESUME_LATITUDE = 200.0;
// Anything faster than that (in meters per second) will be considered moving.
public static final double MAX_NO_MOVEMENT_SPEED = 0.224;
private static final String TAG = TrackRecordingService.class.getSimpleName();
// 1 minute in milliseconds
private static final long ONE_MINUTE = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
public static final double MAX_NO_MOVEMENT_SPEED = 0.224;
// The following variables are set in onCreate:
private ExecutorService executorService;
private ContentProviderUtils contentProviderUtils;
private Handler handler;
private LocationManagerConnector locationManagerConnector;
private LocationManager locationManager;
private PeriodicTaskExecutor voiceExecutor;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceNotificationManager notificationManager;
private LocationListenerPolicy locationListenerPolicy;
private long recordingTrackId;
private boolean recordingTrackPaused;
private LocationListenerPolicy locationListenerPolicy;
private int recordingDistanceInterval;
private int maxRecordingDistance;
private int recordingGpsAccuracy;
private long currentRecordingInterval;
// The following variables are set when recording:
private TripStatisticsUpdater trackTripStatisticsUpdater;
// Note that sharedPreferenceChangeListener cannot be an anonymous inner class; anonymous inner class will get garbage collected.
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
@@ -124,12 +115,12 @@ public class TrackRecordingService extends Service {
int minRecordingInterval = PreferencesUtils.getMinRecordingInterval(context);
if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptBatteryLife(context)) {
// Choose battery life over moving time accuracy.
locationListenerPolicy = new AdaptiveLocationListenerPolicy(30 * UnitConversions.ONE_SECOND, 5 * ONE_MINUTE, 5);
locationListenerPolicy = new AdaptiveLocationListenerPolicy(30 * UnitConversions.ONE_SECOND_MS, 5 * UnitConversions.ONE_MINUTE_MS, 5);
} else if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptAccuracy(context)) {
// Get all the updates.
locationListenerPolicy = new AdaptiveLocationListenerPolicy(UnitConversions.ONE_SECOND, 30 * UnitConversions.ONE_SECOND, 0);
locationListenerPolicy = new AdaptiveLocationListenerPolicy(UnitConversions.ONE_SECOND_MS, 30 * UnitConversions.ONE_SECOND_MS, 0);
} else {
locationListenerPolicy = new AbsoluteLocationListenerPolicy(minRecordingInterval * UnitConversions.ONE_SECOND);
locationListenerPolicy = new AbsoluteLocationListenerPolicy(minRecordingInterval * UnitConversions.ONE_SECOND_MS);
}
}
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
@@ -144,20 +135,21 @@ public class TrackRecordingService extends Service {
}
};
// The following variables are set when recording:
private WakeLock wakeLock;
private BluetoothRemoteSensorManager remoteSensorManager;
private TripStatisticsUpdater trackTripStatisticsUpdater;
private Location lastLocation;
private boolean currentSegmentHasLocation;
private boolean isIdle; // true if idle
private ServiceBinder binder = new ServiceBinder(this);
private LocationListener locationListener = new LocationListener() {
private boolean isIdle;
private TrackRecordingServiceBinder binder = new TrackRecordingServiceBinder(this);
private final LocationListener locationListener = new LocationListener() {
@Override
public void onLocationChanged(final Location location) {
if (locationManagerConnector == null
|| executorService == null
|| executorService.isShutdown()
|| executorService.isTerminated()) {
if (executorService == null || executorService.isShutdown() || executorService.isTerminated()) {
return;
}
executorService.submit(new Runnable() {
@@ -184,35 +176,21 @@ public class TrackRecordingService extends Service {
}
};
private final Runnable registerLocationRunnable = new Runnable() {
@Override
public void run() {
if (isRecording() && !isPaused()) {
registerLocationListener();
}
handler.postDelayed(this, ONE_MINUTE);
}
};
@Override
public void onCreate() {
super.onCreate();
executorService = Executors.newSingleThreadExecutor();
contentProviderUtils = new ContentProviderUtils(this);
handler = new Handler();
locationManagerConnector = new LocationManagerConnector(this, handler.getLooper());
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory());
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
notificationManager = new TrackRecordingServiceNotificationManager(this);
// onSharedPreferenceChanged might not set recordingTrackId.
recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
// Require voiceExecutor and splitExecutor to be created.
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
handler.post(registerLocationRunnable);
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);
@@ -247,11 +225,10 @@ public class TrackRecordingService extends Service {
// Reverse order from onCreate
showNotification(false); //TODO Why?
handler.removeCallbacks(registerLocationRunnable);
unregisterLocationListener();
locationManager = null;
// unregister sharedPreferences before shutting down splitExecutor and voiceExecutor
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
PreferencesUtils.unregister(this, sharedPreferenceChangeListener);
try {
voiceExecutor.shutdown();
@@ -259,14 +236,13 @@ public class TrackRecordingService extends Service {
voiceExecutor = null;
}
locationManagerConnector = null;
contentProviderUtils = null;
binder.detachFromService();
binder = null;
// This should be the next to last operation
releaseWakeLock();
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
// Shutdown the executorService last to avoid sending events to a dead executor.
executorService.shutdown();
@@ -281,6 +257,10 @@ public class TrackRecordingService extends Service {
return recordingTrackPaused;
}
public long getRecordingTrackId() {
return recordingTrackId;
}
public TripStatistics getTripStatistics() {
if (trackTripStatisticsUpdater == null) {
return null;
@@ -288,6 +268,16 @@ public class TrackRecordingService extends Service {
return trackTripStatisticsUpdater.getTripStatistics();
}
public long getTotalTime() {
if (trackTripStatisticsUpdater == null) {
return 0;
}
if (!isPaused()) {
trackTripStatisticsUpdater.updateTime(System.currentTimeMillis());
}
return trackTripStatisticsUpdater.getTripStatistics().getTotalTime();
}
/**
* Inserts a waypoint.
*
@@ -322,9 +312,9 @@ public class TrackRecordingService extends Service {
long duration = stats.getTotalTime();
// Insert waypoint
Waypoint waypoint = new Waypoint(name, description, category, icon, recordingTrackId, length, duration, -1L, -1L, location, null, photoUrl);
Waypoint waypoint = new Waypoint(name, description, category, icon, recordingTrackId, length, duration, location, photoUrl);
Uri uri = contentProviderUtils.insertWaypoint(waypoint);
return Long.parseLong(uri.getLastPathSegment());
return ContentUris.parseId(uri);
}
/**
@@ -332,7 +322,7 @@ public class TrackRecordingService extends Service {
*
* @return the track id
*/
private long startNewTrack() {
long startNewTrack() {
if (isRecording()) {
Log.d(TAG, "Ignore startNewTrack. Already recording.");
return -1L;
@@ -343,7 +333,7 @@ public class TrackRecordingService extends Service {
// Insert a track
Track track = new Track();
Uri uri = contentProviderUtils.insertTrack(track);
long trackId = Long.parseLong(uri.getLastPathSegment());
long trackId = ContentUris.parseId(uri);
// Update shared preferences
updateRecordingState(trackId, false);
@@ -358,37 +348,25 @@ public class TrackRecordingService extends Service {
track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics());
contentProviderUtils.updateTrack(track);
startRecording(true);
startRecording();
return trackId;
}
/**
* Restart a track.
*
* @param track the track
*/
private void restartTrack(Track track) {
Log.d(TAG, "Restarting track: " + track.getId());
TripStatistics tripStatistics = track.getTripStatistics();
trackTripStatisticsUpdater = new TripStatisticsUpdater(tripStatistics.getStartTime());
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
while (locationIterator.hasNext()) {
Location location = locationIterator.next();
trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
}
try (TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY)) {
trackTripStatisticsUpdater.addLocation(locationIterator, recordingDistanceInterval);
} catch (RuntimeException e) {
Log.e(TAG, "RuntimeException", e);
}
startRecording(true);
startRecording();
}
/**
* Resumes current track.
*/
private void resumeCurrentTrack() {
void resumeCurrentTrack() {
if (!isRecording() || !isPaused()) {
Log.d(TAG, "Ignore resumeCurrentTrack. Not recording or not paused.");
return;
@@ -403,20 +381,18 @@ public class TrackRecordingService extends Service {
if (track != null) {
Location resume = new Location(LocationManager.GPS_PROVIDER);
resume.setLongitude(0);
resume.setLatitude(RESUME_LATITUDE);
resume.setLatitude(TrackPointsColumns.RESUME_LATITUDE);
resume.setTime(System.currentTimeMillis());
insertLocation(track, resume, null);
}
startRecording(false);
startRecording();
}
/**
* Common code for starting a new track, resuming a track, or restarting after phone reboot.
*
* @param trackStarted true if track is started, false if track is resumed
*/
private void startRecording(boolean trackStarted) {
private void startRecording() {
// Update instance variables
remoteSensorManager = new BluetoothRemoteSensorManager(this);
remoteSensorManager.start();
@@ -430,19 +406,19 @@ public class TrackRecordingService extends Service {
voiceExecutor.restore();
}
/**
* Starts gps.
*/
void tryStartGps() {
if (isRecording()) return;
startGps();
}
private void startGps() {
wakeLock = SystemUtils.acquireWakeLock(this, wakeLock);
registerLocationListener();
showNotification(true);
}
/**
* Ends the current track.
*/
private void endCurrentTrack() {
void endCurrentTrack() {
if (!isRecording()) {
Log.d(TAG, "Ignore endCurrentTrack. Not recording.");
return;
@@ -463,7 +439,7 @@ public class TrackRecordingService extends Service {
insertLocation(track, lastLocation, getLastValidTrackPointInCurrentSegment(trackId));
// Update the recording track time
updateRecordingTrack(track, contentProviderUtils.getLastTrackPointId(trackId), false);
updateRecordingTrack(track, false);
}
String trackName = TrackNameUtils.getTrackName(this, trackId, track.getTripStatistics().getStartTime());
@@ -475,10 +451,7 @@ public class TrackRecordingService extends Service {
endRecording(true);
}
/**
* Pauses the current track.
*/
private void pauseCurrentTrack() {
void pauseCurrentTrack() {
if (!isRecording() || isPaused()) {
Log.d(TAG, "Ignore pauseCurrentTrack. Not recording or paused.");
return;
@@ -495,7 +468,7 @@ public class TrackRecordingService extends Service {
Location pause = new Location(LocationManager.GPS_PROVIDER);
pause.setLongitude(0);
pause.setLatitude(PAUSE_LATITUDE);
pause.setLatitude(TrackPointsColumns.PAUSE_LATITUDE);
pause.setTime(System.currentTimeMillis());
insertLocation(track, pause, null);
}
@@ -527,13 +500,15 @@ public class TrackRecordingService extends Service {
/**
* Stops gps.
*
* @param stop true to stop self
* @param shutdown true to shutdown self
*/
private void stopGps(boolean stop) {
void stopGps(boolean shutdown) {
if (!isRecording()) return;
unregisterLocationListener();
showNotification(false);
releaseWakeLock();
if (stop) {
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
if (shutdown) {
stopSelf();
}
}
@@ -564,107 +539,94 @@ public class TrackRecordingService extends Service {
PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, recordingTrackPaused);
}
/**
* Called when location changed.
*
* @param location the location
*/
private void onLocationChangedAsync(Location location) {
try {
if (!isRecording() || isPaused()) {
Log.w(TAG, "Ignore onLocationChangedAsync. Not recording or paused.");
return;
}
Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track == null) {
Log.w(TAG, "Ignore onLocationChangedAsync. No track.");
return;
}
if (!LocationUtils.isValidLocation(location)) {
Log.w(TAG, "Ignore onLocationChangedAsync. location is invalid.");
return;
}
notificationManager.updateLocation(this, location, recordingGpsAccuracy);
if (!location.hasAccuracy() || location.getAccuracy() >= 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());
}
Location lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
long idleTime = 0L;
if (lastValidTrackPoint != null && location.getTime() > lastValidTrackPoint.getTime()) {
idleTime = location.getTime() - lastValidTrackPoint.getTime();
}
locationListenerPolicy.updateIdleTime(idleTime);
if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) {
registerLocationListener();
}
SensorDataSet sensorDataSet = getSensorDataSet();
if (sensorDataSet != null) {
location = new SensorDataSetLocation(location, sensorDataSet);
}
// Always insert the first segment location
if (!currentSegmentHasLocation) {
insertLocation(track, location, null);
currentSegmentHasLocation = true;
lastLocation = location;
return;
}
if (!LocationUtils.isValidLocation(lastValidTrackPoint)) {
// Should not happen. The current segment should have a location. Just insert the current location.
insertLocation(track, location, null);
lastLocation = location;
return;
}
double distanceToLastTrackLocation = location.distanceTo(lastValidTrackPoint);
if (distanceToLastTrackLocation > maxRecordingDistance) {
insertLocation(track, lastLocation, lastValidTrackPoint);
Location pause = new Location(LocationManager.GPS_PROVIDER);
pause.setLongitude(0);
pause.setLatitude(PAUSE_LATITUDE);
pause.setTime(lastLocation.getTime());
insertLocation(track, pause, null);
insertLocation(track, location, null);
isIdle = false;
} else if (sensorDataSet != null || distanceToLastTrackLocation >= recordingDistanceInterval) {
insertLocation(track, lastLocation, lastValidTrackPoint);
insertLocation(track, location, null);
isIdle = false;
} else if (!isIdle && location.hasSpeed() && location.getSpeed() < MAX_NO_MOVEMENT_SPEED) {
insertLocation(track, lastLocation, lastValidTrackPoint);
insertLocation(track, location, null);
isIdle = true;
} else if (isIdle && location.hasSpeed() && location.getSpeed() >= MAX_NO_MOVEMENT_SPEED) {
insertLocation(track, lastLocation, lastValidTrackPoint);
insertLocation(track, location, null);
isIdle = false;
} else {
Log.d(TAG, "Not recording location, idle");
}
lastLocation = location;
} catch (Error e) {
Log.e(TAG, "Error in onLocationChangedAsync", e);
throw e;
} catch (RuntimeException e) {
Log.e(TAG, "RuntimeException in onLocationChangedAsync", e);
throw e;
void onLocationChangedAsync(Location location) {
if (!isRecording() || isPaused()) {
Log.w(TAG, "Ignore onLocationChangedAsync. Not recording or paused.");
return;
}
Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track == null) {
Log.w(TAG, "Ignore onLocationChangedAsync. No track.");
return;
}
if (!LocationUtils.isValidLocation(location)) {
Log.w(TAG, "Ignore onLocationChangedAsync. location is invalid.");
return;
}
notificationManager.updateLocation(this, location, recordingGpsAccuracy);
if (!location.hasAccuracy() || location.getAccuracy() >= 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());
}
Location lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
long idleTime = 0L;
if (lastValidTrackPoint != null && location.getTime() > lastValidTrackPoint.getTime()) {
idleTime = location.getTime() - lastValidTrackPoint.getTime();
}
locationListenerPolicy.updateIdleTime(idleTime);
if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) {
registerLocationListener();
}
SensorDataSet sensorDataSet = getSensorDataSet();
if (sensorDataSet != null) {
location = new TrackPoint(location, sensorDataSet);
}
// Always insert the first segment location
if (!currentSegmentHasLocation) {
insertLocation(track, location, null);
currentSegmentHasLocation = true;
lastLocation = location;
return;
}
if (!LocationUtils.isValidLocation(lastValidTrackPoint)) {
// Should not happen. The current segment should have a location. Just insert the current location.
insertLocation(track, location, null);
lastLocation = location;
return;
}
double distanceToLastTrackLocation = location.distanceTo(lastValidTrackPoint);
if (distanceToLastTrackLocation > maxRecordingDistance) {
insertLocation(track, lastLocation, lastValidTrackPoint);
Location pause = new Location(LocationManager.GPS_PROVIDER);
pause.setLongitude(0);
pause.setLatitude(TrackPointsColumns.PAUSE_LATITUDE);
pause.setTime(lastLocation.getTime());
insertLocation(track, pause, null);
insertLocation(track, location, null);
isIdle = false;
} else if (sensorDataSet != null || distanceToLastTrackLocation >= recordingDistanceInterval) {
insertLocation(track, lastLocation, lastValidTrackPoint);
insertLocation(track, location, null);
isIdle = false;
} else if (!isIdle && location.hasSpeed() && location.getSpeed() < MAX_NO_MOVEMENT_SPEED) {
insertLocation(track, lastLocation, lastValidTrackPoint);
insertLocation(track, location, null);
isIdle = true;
} else if (isIdle && location.hasSpeed() && location.getSpeed() >= MAX_NO_MOVEMENT_SPEED) {
insertLocation(track, lastLocation, lastValidTrackPoint);
insertLocation(track, location, null);
isIdle = false;
} else {
Log.d(TAG, "Not recording location, idle");
}
lastLocation = location;
}
/**
@@ -686,10 +648,9 @@ public class TrackRecordingService extends Service {
}
try {
Uri uri = contentProviderUtils.insertTrackPoint(location, track.getId());
long trackPointId = Long.parseLong(uri.getLastPathSegment());
contentProviderUtils.insertTrackPoint(location, track.getId());
trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
updateRecordingTrack(track, trackPointId, LocationUtils.isValidLocation(location));
updateRecordingTrack(track, LocationUtils.isValidLocation(location));
} catch (SQLiteException e) {
/*
* Insert failed, most likely because of SqlLite error code 5 (SQLite_BUSY).
@@ -701,20 +662,13 @@ public class TrackRecordingService extends Service {
}
/**
* Updates the recording track time as well as the startId and the stopId.
* Updates the recording track time.
* Increase the number of points if it is a new and valid track point.
*
* @param track the track
* @param lastTrackPointId the last track point id
* @param increaseNumberOfPoints true to increase the number of points
*/
private void updateRecordingTrack(Track track, long lastTrackPointId, boolean increaseNumberOfPoints) {
if (lastTrackPointId >= 0) {
if (track.getStartId() < 0) {
track.setStartId(lastTrackPointId);
}
track.setStopId(lastTrackPointId);
}
private void updateRecordingTrack(Track track, boolean increaseNumberOfPoints) {
if (increaseNumberOfPoints) {
track.setNumberOfPoints(track.getNumberOfPoints() + 1);
}
@@ -724,57 +678,37 @@ public class TrackRecordingService extends Service {
contentProviderUtils.updateTrack(track);
}
private SensorDataSet getSensorDataSet() {
SensorDataSet getSensorDataSet() {
if (remoteSensorManager == null || !remoteSensorManager.isEnabled() || !remoteSensorManager.isSensorDataSetValid()) {
return null;
}
return remoteSensorManager.getSensorDataSet();
}
/**
* Registers the location listener.
*/
private void registerLocationListener() {
if (locationManagerConnector == null) {
if (locationManager == null) {
Log.e(TAG, "locationManager is null.");
return;
}
try {
long interval = locationListenerPolicy.getDesiredPollingInterval();
locationManagerConnector.requestLocationUpdates(interval, locationListenerPolicy.getMinDistance_m(), locationListener);
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, interval, locationListenerPolicy.getMinDistance_m(), locationListener);
currentRecordingInterval = interval;
} catch (RuntimeException e) {
Log.e(TAG, "Could not register location listener.", e);
} catch (SecurityException e) {
Log.e(TAG, "Could not register location listener; permissions not granted.", e);
}
}
/**
* Unregisters the location manager.
*/
private void unregisterLocationListener() {
if (locationManagerConnector == null) {
if (locationManager == null) {
Log.e(TAG, "locationManager is null.");
return;
}
locationManagerConnector.removeLocationUpdates(locationListener);
}
/**
* Releases the wake lock.
*/
private void releaseWakeLock() {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
wakeLock = null;
}
locationManager.removeUpdates(locationListener);
}
private void showNotification(boolean isGpsStarted) {
if ((isRecording() && isPaused()) || (!isRecording() && !isGpsStarted)) {
stopForeground(true);
}
if (isRecording() && !isPaused()) {
if (isRecording()) {
Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId);
PendingIntent pendingIntent = TaskStackBuilder.create(this)
@@ -784,7 +718,7 @@ public class TrackRecordingService extends Service {
notificationManager.updatePendingIntent(pendingIntent);
notificationManager.updateContent(getString(R.string.gps_starting));
startForeground(NOTIFICATION_ID, notificationManager.getNotification());
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.getNotification());
}
if (!isRecording() && isGpsStarted) {
Intent intent = IntentUtils.newIntent(this, TrackListActivity.class);
@@ -794,109 +728,11 @@ public class TrackRecordingService extends Service {
notificationManager.updatePendingIntent(pendingIntent);
notificationManager.updateContent(getString(R.string.gps_starting));
startForeground(NOTIFICATION_ID, notificationManager.getNotification());
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.getNotification());
}
}
/**
* TODO: There is a bug in Android that leaks Binder instances. This bug is
* especially visible if we have a non-static class, as there is no way to
* nullify reference to the outer class (the service). A workaround is to use
* a static class and explicitly clear service and detach it from the
* underlying Binder. With this approach, we minimize the leak to 24 bytes per
* each service instance. For more details, see the following bug:
* http://code.google.com/p/android/issues/detail?id=6426.
*/
private static class ServiceBinder extends android.os.Binder implements ITrackRecordingService {
private TrackRecordingService trackRecordingService;
public ServiceBinder(TrackRecordingService trackRecordingService) {
this.trackRecordingService = trackRecordingService;
}
@Override
public void startGps() {
if (!trackRecordingService.isRecording()) {
trackRecordingService.startGps();
}
}
public void stopGps() {
if (!trackRecordingService.isRecording()) {
trackRecordingService.stopGps(true);
}
}
@Override
public long startNewTrack() {
return trackRecordingService.startNewTrack();
}
@Override
public void pauseCurrentTrack() {
trackRecordingService.pauseCurrentTrack();
}
@Override
public void resumeCurrentTrack() {
trackRecordingService.resumeCurrentTrack();
}
@Override
public void endCurrentTrack() {
trackRecordingService.endCurrentTrack();
}
@Override
public boolean isRecording() {
return trackRecordingService.isRecording();
}
@Override
public boolean isPaused() {
return trackRecordingService.isPaused();
}
@Override
public long getRecordingTrackId() {
return trackRecordingService.recordingTrackId;
}
@Override
public long getTotalTime() {
TripStatisticsUpdater updater = trackRecordingService.trackTripStatisticsUpdater;
if (updater == null) {
return 0;
}
if (!trackRecordingService.isPaused()) {
updater.updateTime(System.currentTimeMillis());
}
return updater.getTripStatistics().getTotalTime();
}
@Override
public long insertWaypoint(String name, String category, String description, String photoUrl) {
return trackRecordingService.insertWaypoint(name, category, description, photoUrl);
}
@VisibleForTesting
@Override
public void insertTrackPoint(Location location) {
trackRecordingService.onLocationChangedAsync(location);
}
@Override
public SensorDataSet getSensorData() {
return trackRecordingService.getSensorDataSet();
}
/**
* Detaches from the track recording service. Clears the reference to the
* outer class to minimize the leak.
*/
private void detachFromService() {
trackRecordingService = null;
if (!isRecording() && !isGpsStarted) {
stopForeground(true);
notificationManager.cancelNotification();
}
}
}
@@ -0,0 +1,97 @@
package de.dennisguse.opentracks.services;
import android.location.Location;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
/**
* TODO: There is a bug in Android that leaks Binder instances. This bug is
* especially visible if we have a non-static class, as there is no way to
* nullify reference to the outer class (the service). A workaround is to use
* a static class and explicitly clear service and detach it from the
* underlying Binder. With this approach, we minimize the leak to 24 bytes per
* each service instance. For more details, see the following bug:
* http://code.google.com/p/android/issues/detail?id=6426.
*/
class TrackRecordingServiceBinder extends android.os.Binder implements TrackRecordingServiceInterface {
private TrackRecordingService trackRecordingService;
TrackRecordingServiceBinder(TrackRecordingService trackRecordingService) {
this.trackRecordingService = trackRecordingService;
}
@Override
public void startGps() {
trackRecordingService.tryStartGps();
}
public void stopGps() {
trackRecordingService.stopGps(true);
}
@Override
public long startNewTrack() {
return trackRecordingService.startNewTrack();
}
@Override
public void pauseCurrentTrack() {
trackRecordingService.pauseCurrentTrack();
}
@Override
public void resumeCurrentTrack() {
trackRecordingService.resumeCurrentTrack();
}
@Override
public void endCurrentTrack() {
trackRecordingService.endCurrentTrack();
}
@Override
public boolean isRecording() {
return trackRecordingService.isRecording();
}
@Override
public boolean isPaused() {
return trackRecordingService.isPaused();
}
@Override
public long getRecordingTrackId() {
return trackRecordingService.getRecordingTrackId();
}
@Override
public long getTotalTime() {
return trackRecordingService.getTotalTime();
}
@Override
public long insertWaypoint(String name, String category, String description, String photoUrl) {
return trackRecordingService.insertWaypoint(name, category, description, photoUrl);
}
@VisibleForTesting
@Override
public void insertTrackPoint(Location location) {
trackRecordingService.onLocationChangedAsync(location);
}
@Override
public SensorDataSet getSensorData() {
return trackRecordingService.getSensorDataSet();
}
/**
* Detaches from the track recording service. Clears the reference to the
* outer class to minimize the leak.
*/
void detachFromService() {
trackRecordingService = null;
}
}
@@ -49,7 +49,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
private final Runnable callback;
private ITrackRecordingService trackRecordingService;
private TrackRecordingServiceInterface trackRecordingService;
/**
* Constructor.
@@ -86,7 +86,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
/**
* Gets the track recording service if bound. Returns null otherwise
*/
public ITrackRecordingService getServiceIfBound() {
public TrackRecordingServiceInterface getServiceIfBound() {
return trackRecordingService;
}
@@ -96,7 +96,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
*
* @param value the value
*/
private void setTrackRecordingService(ITrackRecordingService value) {
private void setTrackRecordingService(TrackRecordingServiceInterface value) {
trackRecordingService = value;
if (callback != null) {
callback.run();
@@ -123,7 +123,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
} catch (RemoteException e) {
Log.e(TAG, "Failed to bind a death recipient.", e);
}
setTrackRecordingService((ITrackRecordingService) service);
setTrackRecordingService((TrackRecordingServiceInterface) service);
}
@Override
@@ -180,7 +180,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
* Resumes the recording track.
*/
public void resumeTrack() {
ITrackRecordingService service = getServiceIfBound();
TrackRecordingServiceInterface service = getServiceIfBound();
if (service != null) {
service.resumeCurrentTrack();
}
@@ -190,7 +190,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
* Pauses the recording track.
*/
public void pauseTrack() {
ITrackRecordingService service = getServiceIfBound();
TrackRecordingServiceInterface service = getServiceIfBound();
if (service != null) {
service.pauseCurrentTrack();
}
@@ -212,7 +212,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
* @return the id of the marker or -1L if none could be created.
*/
public long addMarker(Context context, String name, String category, String description, String photoUrl) {
ITrackRecordingService trackRecordingService = getServiceIfBound();
TrackRecordingServiceInterface trackRecordingService = getServiceIfBound();
if (trackRecordingService == null) {
Log.d(TAG, "Unable to add marker, no track recording service");
} else {
@@ -238,7 +238,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
* @param showEditor true to show the editor
*/
public void stopRecording(@NonNull Context context, boolean showEditor) {
ITrackRecordingService trackRecordingService = getServiceIfBound();
TrackRecordingServiceInterface trackRecordingService = getServiceIfBound();
if (trackRecordingService == null) {
resetRecordingState(context);
} else {
@@ -25,7 +25,7 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
* App's service.
* This service is the process that actually records and manages tracks.
*/
public interface ITrackRecordingService {
public interface TrackRecordingServiceInterface {
/**
* Starts gps.
@@ -20,13 +20,13 @@ import de.dennisguse.opentracks.util.StringUtils;
*/
class TrackRecordingServiceNotificationManager {
private final static int NOTIFICATION_ID = 123;
static final int NOTIFICATION_ID = 123;
private final static String CHANNEL_ID = TrackRecordingServiceNotificationManager.class.getSimpleName();
private static final String CHANNEL_ID = TrackRecordingServiceNotificationManager.class.getSimpleName();
private NotificationCompat.Builder notificationBuilder;
private final NotificationCompat.Builder notificationBuilder;
private NotificationManager notificationManager;
private final NotificationManager notificationManager;
private boolean previousLocationWasAccurate = true;
@@ -76,6 +76,7 @@ class TrackRecordingServiceNotificationManager {
notificationBuilder.setContentText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
updateNotification();
notificationBuilder.setOnlyAlertOnce(true);
}
@@ -84,6 +85,10 @@ class TrackRecordingServiceNotificationManager {
updateNotification();
}
void cancelNotification() {
notificationManager.cancel(NOTIFICATION_ID);
}
Notification getNotification() {
return notificationBuilder.build();
}
@@ -57,9 +57,9 @@ public class BluetoothConnectionManager {
private SensorState sensorState;
private BluetoothGatt bluetoothGatt;
private BluetoothDevice bluetoothDevice;
private final BluetoothDevice bluetoothDevice;
private BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
switch (newState) {
@@ -61,11 +61,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
stop = false;
break;
case AudioManager.AUDIOFOCUS_LOSS:
stop = true;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
stop = true;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
stop = true;
break;
@@ -74,7 +74,7 @@ public class BluetoothLeAdapter extends BaseAdapter {
public class Device {
private String name;
private String address;
private final String address;
public Device(String name, String address) {
this.name = name;
@@ -32,9 +32,9 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
*/
public class BluetoothLePreference extends DialogPreference {
private final static String TAG = BluetoothLePreference.class.getSimpleName();
private static final String TAG = BluetoothLePreference.class.getSimpleName();
private final static int DEVICE_NONE_RESOURCEID = R.string.value_none;
private static final int DEVICE_NONE_RESOURCEID = R.string.value_none;
public BluetoothLePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
@@ -88,10 +88,10 @@ public class BluetoothLePreference extends DialogPreference {
public static class BluetoothLePreferenceDialog extends PreferenceDialogFragmentCompat {
private int selectedEntryIndex;
private BluetoothLeAdapter listAdapter = new BluetoothLeAdapter();
private final BluetoothLeAdapter listAdapter = new BluetoothLeAdapter();
private BluetoothLeScanner scanner = null;
private ScanCallback scanCallback = new ScanCallback() {
private final ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
@@ -50,10 +50,6 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
public static class PrefsFragment extends PreferenceFragmentCompat {
/*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner class.
* Anonymous inner class will get garbage collected.
*/
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
@@ -26,10 +26,6 @@ import androidx.annotation.NonNull;
*/
public class TripStatistics {
// The min and max latitude seen in this trip.
private final ExtremityMonitor latitudeExtremities = new ExtremityMonitor();
// The min and max longitude seen in this trip.
private final ExtremityMonitor longitudeExtremities = new ExtremityMonitor();
// The min and max elevation (meters) seen on this trip.
private final ExtremityMonitor elevationExtremities = new ExtremityMonitor();
// The min and max grade seen on this trip.
@@ -39,16 +35,16 @@ public class TripStatistics {
private long startTime = -1L;
// The trip stop time. This is the system time, might not match the GPS time.
private long stopTime = -1L;
// The total trip distance (meters).
private double totalDistance;
// The total time (ms). Updated when new points are received, may be stale.
private long totalTime;
// The total moving time (ms). Based on when we believe the user is traveling.
private long movingTime;
private double totalDistance_m;
// Updated when new points are received, may be stale.
private long totalTime_ms;
// Based on when we believe the user is traveling.
private long movingTime_ms;
// The maximum speed (meters/second) that we believe is valid.
private double maxSpeed;
private double maxSpeed_mps;
// The total elevation gained (meters).
private double totalElevationGain;
private double totalElevationGain_m;
public TripStatistics() {
}
@@ -61,14 +57,12 @@ public class TripStatistics {
public TripStatistics(TripStatistics other) {
startTime = other.startTime;
stopTime = other.stopTime;
totalDistance = other.totalDistance;
totalTime = other.totalTime;
movingTime = other.movingTime;
latitudeExtremities.set(other.latitudeExtremities.getMin(), other.latitudeExtremities.getMax());
longitudeExtremities.set(other.longitudeExtremities.getMin(), other.longitudeExtremities.getMax());
maxSpeed = other.maxSpeed;
totalDistance_m = other.totalDistance_m;
totalTime_ms = other.totalTime_ms;
movingTime_ms = other.movingTime_ms;
maxSpeed_mps = other.maxSpeed_mps;
elevationExtremities.set(other.elevationExtremities.getMin(), other.elevationExtremities.getMax());
totalElevationGain = other.totalElevationGain;
totalElevationGain_m = other.totalElevationGain_m;
gradeExtremities.set(other.gradeExtremities.getMin(), other.gradeExtremities.getMax());
}
@@ -81,23 +75,15 @@ public class TripStatistics {
public void merge(TripStatistics other) {
startTime = Math.min(startTime, other.startTime);
stopTime = Math.max(stopTime, other.stopTime);
totalDistance += other.totalDistance;
totalTime += other.totalTime;
movingTime += other.movingTime;
if (other.latitudeExtremities.hasData()) {
latitudeExtremities.update(other.latitudeExtremities.getMin());
latitudeExtremities.update(other.latitudeExtremities.getMax());
}
if (other.longitudeExtremities.hasData()) {
longitudeExtremities.update(other.longitudeExtremities.getMin());
longitudeExtremities.update(other.longitudeExtremities.getMax());
}
maxSpeed = Math.max(maxSpeed, other.maxSpeed);
totalDistance_m += other.totalDistance_m;
totalTime_ms += other.totalTime_ms;
movingTime_ms += other.movingTime_ms;
maxSpeed_mps = Math.max(maxSpeed_mps, other.maxSpeed_mps);
if (other.elevationExtremities.hasData()) {
elevationExtremities.update(other.elevationExtremities.getMin());
elevationExtremities.update(other.elevationExtremities.getMax());
}
totalElevationGain += other.totalElevationGain;
totalElevationGain_m += other.totalElevationGain_m;
if (other.gradeExtremities.hasData()) {
gradeExtremities.update(other.gradeExtremities.getMin());
gradeExtremities.update(other.gradeExtremities.getMax());
@@ -111,54 +97,28 @@ public class TripStatistics {
return startTime;
}
/**
* Sets the trip start time.
*
* @param startTime the trip start time in milliseconds since the epoch
*/
public void setStartTime(long startTime) {
this.startTime = startTime;
}
/**
* Gets the trip stop time. The number of milliseconds since epoch.
*/
public long getStopTime() {
return stopTime;
}
/**
* Sets the trip stop time.
*
* @param stopTime the stop time in milliseconds since the epoch
*/
public void setStopTime(long stopTime) {
this.stopTime = stopTime;
}
/**
* Gets the total distance the user traveled in meters.
*/
public double getTotalDistance() {
return totalDistance;
return totalDistance_m;
}
/**
* Sets the total trip distance.
*
* @param totalDistance the trip distance in meters
*/
public void setTotalDistance(double totalDistance) {
this.totalDistance = totalDistance;
public void setTotalDistance(double totalDistance_m) {
this.totalDistance_m = totalDistance_m;
}
/**
* Adds to the current total distance.
*
* @param distance the distance to add in meters
*/
public void addTotalDistance(double distance) {
totalDistance += distance;
public void addTotalDistance(double distance_m) {
totalDistance_m += distance_m;
}
/**
@@ -167,144 +127,23 @@ public class TripStatistics {
* If you need to calculate the proper total time, use {@link #getStartTime} with the current time.
*/
public long getTotalTime() {
return totalTime;
return totalTime_ms;
}
/**
* Sets the trip total time.
*
* @param totalTime the trip total time in milliseconds
*/
public void setTotalTime(long totalTime) {
this.totalTime = totalTime;
public void setTotalTime(long totalTime_ms) {
this.totalTime_ms = totalTime_ms;
}
/**
* Gets the moving time in milliseconds.
*/
public long getMovingTime() {
return movingTime;
return movingTime_ms;
}
/**
* Sets the trip total moving time.
*
* @param movingTime the trip total moving time in milliseconds
*/
public void setMovingTime(long movingTime) {
this.movingTime = movingTime;
public void setMovingTime(long movingTime_ms) {
this.movingTime_ms = movingTime_ms;
}
/**
* Adds to the trip total moving time.
*
* @param time the time in milliseconds
*/
public void addMovingTime(long time) {
movingTime += time;
}
/**
* Gets the topmost position (highest latitude) of the track, in signed degrees.
*/
public double getTopDegrees() {
return latitudeExtremities.getMax();
}
/**
* Gets the topmost position (highest latitude) of the track, in signed millions of degrees.
*/
public int getTop() {
return (int) (latitudeExtremities.getMax() * 1E6);
}
/**
* Gets the bottommost position (lowest latitude) of the track, in signed
* degrees.
*/
public double getBottomDegrees() {
return latitudeExtremities.getMin();
}
/**
* Gets the bottommost position (lowest latitude) of the track, in signed millions of degrees.
*/
public int getBottom() {
return (int) (latitudeExtremities.getMin() * 1E6);
}
/**
* Gets the leftmost position (lowest longitude) of the track, in signed degrees.
*/
public double getLeftDegrees() {
return longitudeExtremities.getMin();
}
/**
* Gets the leftmost position (lowest longitude) of the track, in signed millions of degrees.
*/
public int getLeft() {
return (int) (longitudeExtremities.getMin() * 1E6);
}
/**
* Gets the rightmost position (highest longitude) of the track, in signed degrees.
*/
public double getRightDegrees() {
return longitudeExtremities.getMax();
}
/**
* Gets the rightmost position (highest longitude) of the track, in signed millions of degrees.
*/
public int getRight() {
return (int) (longitudeExtremities.getMax() * 1E6);
}
/**
* Gets the mean latitude position of the track, in signed degrees.
*/
public double getMeanLatitude() {
return (getBottomDegrees() + getTopDegrees()) / 2.0;
}
/**
* Gets the mean longitude position of the track, in signed degrees.
*/
public double getMeanLongitude() {
return (getLeftDegrees() + getRightDegrees()) / 2.0;
}
/**
* Sets the bounding box for this trip. The unit for all parameters is signed
* millions of degree (degrees * 1E6).
*
* @param leftE6 the leftmost longitude reached
* @param topE6 the topmost latitude reached
* @param rightE6 the rightmost longitude reached
* @param bottomE6 the bottommost latitude reached
*/
public void setBounds(int leftE6, int topE6, int rightE6, int bottomE6) {
latitudeExtremities.set(bottomE6 / 1E6, topE6 / 1E6);
longitudeExtremities.set(leftE6 / 1E6, rightE6 / 1E6);
}
/**
* Updates a new latitude value.
*
* @param latitude the latitude value in signed decimal degrees
*/
public void updateLatitudeExtremities(double latitude) {
latitudeExtremities.update(latitude);
}
/**
* Updates a new longitude value.
*
* @param longitude the longitude value in signed decimal degrees
*/
public void updateLongitudeExtremities(double longitude) {
longitudeExtremities.update(longitude);
public void addMovingTime(long time_ms) {
movingTime_ms += time_ms;
}
/**
@@ -312,27 +151,27 @@ public class TripStatistics {
* This calculation only takes into account the displacement until the last point that was accounted for in statistics.
*/
public double getAverageSpeed() {
if (totalTime == 0L) {
if (totalTime_ms == 0L) {
return 0.0;
}
return totalDistance / ((double) totalTime / 1000.0);
return totalDistance_m / ((double) totalTime_ms / 1000.0);
}
/**
* Gets the average moving speed in meters/second.
*/
public double getAverageMovingSpeed() {
if (movingTime == 0L) {
if (movingTime_ms == 0L) {
return 0.0;
}
return totalDistance / ((double) movingTime / 1000.0);
return totalDistance_m / ((double) movingTime_ms / 1000.0);
}
/**
* Gets the maximum speed in meters/second.
*/
public double getMaxSpeed() {
return Math.max(maxSpeed, getAverageMovingSpeed());
return Math.max(maxSpeed_mps, getAverageMovingSpeed());
}
/**
@@ -341,7 +180,7 @@ public class TripStatistics {
* @param maxSpeed the maximum speed in meters/second
*/
public void setMaxSpeed(double maxSpeed) {
this.maxSpeed = maxSpeed;
this.maxSpeed_mps = maxSpeed;
}
/**
@@ -372,65 +211,44 @@ public class TripStatistics {
/**
* Sets the maximum elevation.
*
* @param elevation the maximum elevation in meters
* @param elevation_m the maximum elevation in meters
*/
public void setMaxElevation(double elevation) {
elevationExtremities.setMax(elevation);
public void setMaxElevation(double elevation_m) {
elevationExtremities.setMax(elevation_m);
}
/**
* Updates a new elevation.
*
* @param elevation the elevation value in meters
* @param elevation_m the elevation value in meters
*/
public void updateElevationExtremities(double elevation) {
elevationExtremities.update(elevation);
public void updateElevationExtremities(double elevation_m) {
elevationExtremities.update(elevation_m);
}
/**
* Gets the total elevation gain in meters. This is calculated as the sum of all positive differences in the smoothed elevation.
*/
public double getTotalElevationGain() {
return totalElevationGain;
return totalElevationGain_m;
}
/**
* Sets the total elevation gain.
*
* @param totalElevationGain the elevation gain in meters
*/
public void setTotalElevationGain(double totalElevationGain) {
this.totalElevationGain = totalElevationGain;
public void setTotalElevationGain(double totalElevationGain_m) {
this.totalElevationGain_m = totalElevationGain_m;
}
/**
* Adds to the total elevation gain.
*
* @param gain the elevation gain in meters
*/
public void addTotalElevationGain(double gain) {
totalElevationGain += gain;
public void addTotalElevationGain(double gain_m) {
totalElevationGain_m += gain_m;
}
/**
* Gets the minimum grade for this trip.
*/
public double getMinGrade() {
return gradeExtremities.getMin();
}
/**
* Sets the minimum grade.
*
* @param grade the grade as a fraction (-1.0 would mean vertical downwards)
*/
public void setMinGrade(double grade) {
gradeExtremities.setMin(grade);
}
/**
* Gets the maximum grade for this trip.
*/
public double getMaxGrade() {
return gradeExtremities.getMax();
}
@@ -444,11 +262,6 @@ public class TripStatistics {
gradeExtremities.setMax(grade);
}
/**
* Updates a new grade value.
*
* @param grade the grade value as a fraction
*/
public void updateGradeExtremities(double grade) {
gradeExtremities.update(grade);
}
@@ -458,9 +271,7 @@ public class TripStatistics {
public String toString() {
return "TripStatistics { Start Time: " + getStartTime() + "; Stop Time: " + getStopTime()
+ "; Total Distance: " + getTotalDistance() + "; Total Time: " + getTotalTime()
+ "; Moving Time: " + getMovingTime() + "; Min Latitude: " + getBottomDegrees()
+ "; Max Latitude: " + getTopDegrees() + "; Min Longitude: " + getLeftDegrees()
+ "; Max Longitude: " + getRightDegrees() + "; Max Speed: " + getMaxSpeed()
+ "; Moving Time: " + getMovingTime() + "; Max Speed: " + getMaxSpeed()
+ "; Min Elevation: " + getMinElevation() + "; Max Elevation: " + getMaxElevation()
+ "; Elevation Gain: " + getTotalElevationGain() + "; Min Grade: " + getMinGrade()
+ "; Max Grade: " + getMaxGrade() + "}";
@@ -21,10 +21,11 @@ import android.util.Log;
import androidx.annotation.VisibleForTesting;
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 static de.dennisguse.opentracks.services.TrackRecordingService.PAUSE_LATITUDE;
/**
* Updater for {@link TripStatistics}.
@@ -122,7 +123,7 @@ public class TripStatisticsUpdater {
updateTime(location.getTime());
if (!LocationUtils.isValidLocation(location)) {
// Either pause or resume marker
if (location.getLatitude() == PAUSE_LATITUDE) {
if (location.getLatitude() == TrackPointsColumns.PAUSE_LATITUDE) {
if (lastLocation != null && lastMovingLocation != null && lastLocation != lastMovingLocation) {
currentSegment.addTotalDistance(lastMovingLocation.distanceTo(lastLocation));
}
@@ -138,9 +139,6 @@ public class TripStatisticsUpdater {
return;
}
currentSegment.updateLatitudeExtremities(location.getLatitude());
currentSegment.updateLongitudeExtremities(location.getLongitude());
//TODO Use Barometer to compute elevation gain.
double elevationDifference = location.hasAltitude() ? updateElevation(location.getAltitude()) : 0.0;
@@ -181,6 +179,13 @@ public class TripStatisticsUpdater {
lastMovingLocation = location;
}
public void addLocation(TrackPointIterator iterator, int minRecordingDistance) {
while (iterator.hasNext()) {
Location location = iterator.next();
addLocation(location, minRecordingDistance);
}
}
/**
* Gets the smoothed elevation over several readings.
* The elevation readings is noisy so the smoothed elevation is better than the raw elevation for many tasks.

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