Compare commits

..

82 Commits

Author SHA1 Message Date
Dennis Guse e49a425106 Release v3.6.1 2020-04-12 18:46:49 +02:00
Román adbad04867 URIs for waypoint's photos are correct. Fixes #183. 2020-04-12 18:27:20 +02:00
Dennis Guse 850bd2f887 Update Android gradle plugin. 2020-04-11 23:16:38 +02:00
Dennis Guse d240b457e2 Release v3.6.0 2020-04-10 14:55:04 +02:00
Dennis Guse 6d800caab1 While importing photos, check url carefully.
(cherry picked from commit 309c96e4789bd7623b84720a8c8f2bf297c95d59)
2020-04-10 14:42:11 +02:00
Román 7bf153db39 Avoid NullPointer in creating photoUrl when importing 2020-04-10 14:42:11 +02:00
Román 1b69f29751 Importing creates content:// for attached pictures. Also, exporting without points bug fixed and images href contains relative link. Fixes #166 and #176. 2020-04-10 14:42:11 +02:00
Dennis Guse dfc3837739 Settings: PrefsFragment could not be re-created. Fixes #179. 2020-04-08 08:10:46 +02:00
Dennis Guse 30e847866e Removed TravisCI it does not work as desired. 2020-04-07 21:01:54 +02:00
Dennis Guse fbc902e830 Cleanup using AndroidStudio code analyzer.
Moved to iterator for loops for ChartView and ChartFragment.
2020-04-05 21:56:26 +02:00
Dennis Guse 208f1d71ba Refactor ChartView/ChartFragment to use ChartPoint. 2020-04-05 21:56:26 +02:00
Dennis Guse 38a15a96b3 Cleanup. 2020-04-05 21:56:26 +02:00
Dennis Guse 46f8753746 Removed TrackDataType and simplified TrackDataManager. 2020-04-05 21:56:26 +02:00
Dennis Guse 70bc8f33b1 Do not expose TrackDataType to fragments. 2020-04-05 21:56:26 +02:00
Dennis Guse 6cd27ab096 Merged DataSourceManager into TrackDataHub. 2020-04-05 21:56:26 +02:00
Dennis Guse 5581b404f7 DataSourceManager now always registers for all changes. 2020-04-05 21:56:26 +02:00
Dennis Guse 2ef74eb908 Automated code cleanup using AndroidStudio. 2020-04-05 21:56:26 +02:00
Dennis Guse 16c654141f Renamed TripStatistics to TrackStatistics. 2020-04-05 21:56:26 +02:00
Dennis Guse dfc785f2fd Simplified internal data processing (class structure). 2020-04-05 21:56:26 +02:00
Dennis Guse f621cdf032 Removed grade internals completely. 2020-04-05 21:56:26 +02:00
Román 8b04d9c4a6 creates a segment like pause/continue. 2020-04-05 14:25:29 +02:00
Dennis Guse 364b4702e3 Waypoint requires a non-null location.
Remove setLocation() from Waypoint and TrackPoint.
2020-03-30 19:52:06 +02:00
Dennis Guse a98c16de28 Removed TrackPointFactory.
New TrackPoints are now always created and old ones not reused.
2020-03-30 19:52:06 +02:00
Dennis Guse 881bf1982a Removed TrackWriterTrackPointFactory.
Replaced with local variable for TrackPoint swapping.
2020-03-30 19:52:06 +02:00
Dennis Guse f340622977 Automated code analysis improvements. 2020-03-30 19:52:06 +02:00
Dennis Guse 194379295d TrackPoint has Location as delegate instead of inheritance. 2020-03-30 19:52:06 +02:00
Dennis Guse 901cffee97 Use TrackPoint instead of Location (and omit casting). 2020-03-30 19:52:06 +02:00
Román d93c2ec5f3 TrackDetailActivity starts an actual recording and a stopped recording can be resumed. Fixes #103 and #11. 2020-03-30 19:25:22 +02:00
Dennis Guse 306522863a Release v3.5.0 2020-03-21 23:18:57 +01:00
Dennis Guse e54d85e04d ChartFragment: unregister from preference changes on pause. 2020-03-21 20:54:49 +01:00
Dennis Guse cb30ad27c3 Removed that tracks store count of the locations. 2020-03-20 19:44:31 +01:00
Dennis Guse 8685c9cc88 Merge pull request #162 from rgmf/issue#89
Share pictures from marker.
2020-03-18 22:08:42 +01:00
Román a6f03d0f23 Share pictures from marker.
Fixes #89.
2020-03-18 19:33:57 +01:00
Dennis Guse e3c812b155 TrackDeleteActivity now has a transparant background. 2020-03-18 07:43:21 +01:00
Dennis Guse f149de9ce3 Merge pull request #167 from rgmf/issue#163
Add android:key in PreferenceCategory with expandable.
2020-03-17 21:54:08 +01:00
Dennis Guse 3447dd2569 Merge pull request #165 from OpenTracksApp/dbCleanup
Database cleanup and optimization
2020-03-17 21:44:28 +01:00
Dennis Guse b2b8c7fd54 Database upgrade (incl. tests): removed unused columns and added indices.
Fixes #108.
2020-03-17 21:39:10 +01:00
Román 486bcd0aec Add android:key in PreferenceCategory with expandable.
Fixes #163.
2020-03-17 10:57:04 +01:00
Dennis Guse c8a33201f5 Moved annoucement text generation into util class. 2020-03-16 21:43:23 +01:00
Dennis Guse 4a37c2cc7e Cleanup. 2020-03-16 21:43:23 +01:00
Dennis Guse d23db5bafd Charts use preferred units and rate.
Fixes #150.
2020-03-16 21:43:23 +01:00
Dennis Guse ac528f9a10 Use trackname for sharing files.
Fixes #130.
2020-03-16 21:43:23 +01:00
Dennis Guse 256eadd49d Added some french translation provided by @edern-appere.
Fixes #147.
2020-03-16 21:43:23 +01:00
Dennis Guse 64e58934a5 Settings: fix label for elevation (removed "Grade" as it is not shown anymore). 2020-03-16 21:43:23 +01:00
Dennis Guse 92dfadfdba Remove elevation min/max from UI as GPS-derived data often lead to invalid data (especially initial fix phase).
Fixes #148.
2020-03-16 21:43:23 +01:00
Román cbce70143b All marker management is now in MarkerEditActivity (including take photo and added gallery image to the marker).
Issue 122.
2020-03-11 19:59:15 +01:00
Román f4b9e3159e New screenshots for F-Droid/PlayStore. Fixes #132. 2020-03-11 19:40:57 +01:00
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
205 changed files with 3834 additions and 4201 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
-5
View File
@@ -45,19 +45,16 @@ captures/
# IntelliJ IDEA
*.iml
*.iws
/out/
# User-specific configurations
.idea/.name
.idea/assetWizardSettings.xml
.idea/caches/
.idea/compiler.xml
.idea/copyright/profiles_settings.xml
.idea/dataSources.ids
.idea/datasources.xml
.idea/dictionaries
.idea/dynamic.xml
.idea/encodings.xml
.idea/jsLibraryMappings.xml
@@ -67,10 +64,8 @@ captures/
.idea/scopes/scope_settings.xml
.idea/shelf/
.idea/sqlDataSources.xml
.idea/tasks.xml
.idea/vcs.xml
.idea/uiDesigner.xml
.idea/workspace.xml
# Keystore files
*.jks
-46
View File
@@ -1,46 +0,0 @@
#See https://docs.travis-ci.com/user/languages/android/
#See https://github.com/ankidroid/Anki-Android/blob/master/.travis.yml
language: android
env:
global:
- ADB_INSTALL_TIMEOUT=8
- ABI=armeabi-v7a
- EMU_FLAVOR=default
# PATH order is incredibly important. e.g. the 'emulator' script exists in more than one place!
- ANDROID_HOME=/usr/local/android-sdk
- TOOLS=${ANDROID_HOME}/tools
- PATH=${ANDROID_HOME}:${ANDROID_HOME}/emulator:${TOOLS}:${TOOLS}/bin:${ANDROID_HOME}/platform-tools:${PATH}
matrix:
- API=27 ABI=x86_64
android:
components:
# installing tools to start, then use `sdkmanager` below to get the rest
- tools
- build-tools-28.0.3
- 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 &
script:
- ./gradlew clean build
# - ./gradlew connectedCheck
sbefore_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
- $HOME/.android/build-cache
+3 -3
View File
@@ -14,15 +14,15 @@ For the time being, the OpenTracks Data API can be used to implement mainly cust
## Implementation
OpenTracks by default does not expose any data to 3rd-party applications.
On the request of the user (i.e., pressing a button), an Intent is created that exposes the `Track` data (it's summary) as well as the `TrackPoint`s.
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 is exposed,
* 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 impossible, and
* no write access is possible, and
* neither track recordings can be started, paused, or stopped.
All required information can be found in `IntentDashboardUtils.java`.
+1 -1
View File
@@ -4,7 +4,7 @@
1. Get next _version code_ (master branch): `git rev-list HEAD --count master`
2. Decide on _version name_ (semantic versioning)
3. Manually update _version code_ and _version name_ in `AndroidManifest.xml` (used by FDroid)
3. Manually update _version code_ and _version name_ in `build.gradle`
4. Create changelog (`_version code_.txt`)
5. Create commit with all changes
6. Tag newly create commit with _version name_ (e.g., v3.2.1)
+8 -4
View File
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.android.tools.build:gradle:3.6.2'
}
}
@@ -59,11 +59,14 @@ android {
applicationName: "@string/app_name"
]
//Useful for debugging.
//versionCode getVersionCode()
versionCode 3333
//versionCode getVersionCode() //Useful for debugging.
versionName "v3.6.1"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdkVersion 21
targetSdkVersion 29
@@ -98,6 +101,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'
@@ -109,5 +113,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
@@ -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)
@@ -0,0 +1,14 @@
v3.5.0: OpenTracks
Changes:
- Marker: select pictures from gallery
- Marker: share picture
- Use track name as filename when sharing
- Removed elevation min/max (was imprecise)
Bugfix:
- Charts use preferred units and rate
Developer:
- Database: remove unused columns
- Database: add indices
@@ -0,0 +1,10 @@
v3.6.0: OpenTracks
Changes:
- Stopped tracks can now be resumed
Bugfix:
- Import of KMZ did not process the photo URL correctly
Developer:
- Simplified data processing
@@ -0,0 +1,4 @@
v3.6.1: OpenTracks
Bugfix:
- KMZ import: store correct picture path
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

+2 -1
View File
@@ -1,5 +1,6 @@
#Sun Apr 12 18:41:12 CEST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
@@ -49,7 +49,7 @@ public class TrackStubUtils {
public static Track createTrack(int numberOfLocations) {
Track track = new Track();
for (int i = 0; i < numberOfLocations; i++) {
track.addLocation(createSensorDataSetLocation(INITIAL_LATITUDE + i * DIFFERENCE, INITIAL_LONGITUDE + i * DIFFERENCE, INITIAL_ALTITUDE + i * DIFFERENCE));
track.addTrackPoint(createDefaultTrackPoint(INITIAL_LATITUDE + i * DIFFERENCE, INITIAL_LONGITUDE + i * DIFFERENCE, INITIAL_ALTITUDE + i * DIFFERENCE));
}
return track;
@@ -60,8 +60,8 @@ public class TrackStubUtils {
*
* @return a track stub.
*/
public static TrackPoint createSensorDataSetLocation() {
return createSensorDataSetLocation(INITIAL_LATITUDE, INITIAL_LONGITUDE, INITIAL_ALTITUDE);
public static TrackPoint createDefaultTrackPoint() {
return createDefaultTrackPoint(INITIAL_LATITUDE, INITIAL_LONGITUDE, INITIAL_ALTITUDE);
}
/**
@@ -69,18 +69,16 @@ public class TrackStubUtils {
*
* @return a SensorDataSetLocation stub.
*/
private static TrackPoint createSensorDataSetLocation(double latitude, double longitude, double altitude) {
// Initial Location
Location loc = new Location(LOCATION_PROVIDER);
loc.setLatitude(latitude);
loc.setLongitude(longitude);
loc.setAltitude(altitude);
loc.setAccuracy(INITIAL_ACCURACY);
loc.setSpeed(INITIAL_SPEED);
loc.setTime(INITIAL_TIME);
loc.setBearing(INITIAL_BEARING);
SensorDataSet sd = new SensorDataSet(Float.NaN, Float.NaN);
private static TrackPoint createDefaultTrackPoint(double latitude, double longitude, double altitude) {
Location location = new Location(LOCATION_PROVIDER);
location.setLatitude(latitude);
location.setLongitude(longitude);
location.setAltitude(altitude);
location.setAccuracy(INITIAL_ACCURACY);
location.setSpeed(INITIAL_SPEED);
location.setTime(INITIAL_TIME);
location.setBearing(INITIAL_BEARING);
return new TrackPoint(loc, sd);
return new TrackPoint(location, new SensorDataSet(Float.NaN, Float.NaN));
}
}
@@ -15,6 +15,7 @@
*/
package de.dennisguse.opentracks.chart;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -44,7 +45,17 @@ public class ChartValueSeriesTest {
R.string.description_elevation_metric,
R.string.description_elevation_imperial,
R.color.chart_elevation_fill,
R.color.chart_elevation_border);
R.color.chart_elevation_border) {
@Override
double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getElevation();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return false;
}
};
}
@Test
@@ -63,8 +74,8 @@ public class ChartValueSeriesTest {
@Test
public void testSmallUpdates() {
series.update(0);
series.update(10);
series.update(new ChartPoint(0));
series.update(new ChartPoint(10));
series.updateDimension();
Assert.assertEquals(100, series.getInterval());
Assert.assertEquals(0, series.getMinMarkerValue());
@@ -73,8 +84,8 @@ public class ChartValueSeriesTest {
@Test
public void testBigUpdates() {
series.update(0);
series.update(901);
series.update(new ChartPoint(0));
series.update(new ChartPoint(901));
series.updateDimension();
Assert.assertEquals(1000, series.getInterval());
Assert.assertEquals(0, series.getMinMarkerValue());
@@ -83,8 +94,8 @@ public class ChartValueSeriesTest {
@Test
public void testNotZeroBasedUpdates() {
series.update(220);
series.update(250);
series.update(new ChartPoint(220));
series.update(new ChartPoint(250));
series.updateDimension();
Assert.assertEquals(100, series.getInterval());
Assert.assertEquals(200, series.getMinMarkerValue());
@@ -1,148 +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.content;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
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;
/**
* Tests {@link CustomContentProvider}.
*
* @author Youtao Liu
*/
public class CustomContentProviderTest {
private static final String DATABASE_NAME = "test.db";
private SQLiteDatabase db;
private CustomContentProvider customContentProvider;
private Context context = ApplicationProvider.getApplicationContext();
@Before
public void setUp() {
context.deleteDatabase(DATABASE_NAME);
db = (new DatabaseHelper(context, DATABASE_NAME)).getWritableDatabase();
customContentProvider = new CustomContentProvider() {
};
}
/**
* Tests {@link CustomContentProvider.DatabaseHelper#onCreate(SQLiteDatabase)}.
*/
@Test
public void testDatabaseHelper_OnCreate() {
Assert.assertTrue(hasTable(TracksColumns.TABLE_NAME));
Assert.assertTrue(hasTable(TrackPointsColumns.TABLE_NAME));
Assert.assertTrue(hasTable(WaypointsColumns.TABLE_NAME));
}
/**
* Tests {@link CustomContentProvider#onCreate(android.content.Context)}.
*/
@Test
public void testOnCreate() {
Assert.assertTrue(customContentProvider.onCreate(context));
}
/**
* Tests {@link CustomContentProvider#getType(Uri)}.
*/
@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(WaypointsColumns.CONTENT_TYPE, customContentProvider.getType(WaypointsColumns.CONTENT_URI));
}
/**
* Creates a table, containing one column.
*
* @param table the table name
*/
private void createTable(String table) {
db.execSQL("CREATE TABLE " + table + " (test INTEGER)");
}
/**
* Drops a table in database.
*
* @param table the table name
*/
private void dropTable(String table) {
db.execSQL("Drop TABLE " + table);
}
/**
* Returns true if the table exists.
*
* @param table the table name
*/
private boolean hasTable(String table) {
try {
db.rawQuery("select count(*) from " + table, null);
return true;
} catch (Exception e) {
return false;
}
}
/**
* Returns true if the column in the table exists.
*
* @param table the table name
* @param column the column name
*/
private boolean hasColumn(String table, String column) {
try {
db.execSQL("SElECT count(*) from " + table + " order by " + column);
} catch (Exception e) {
if (e.getMessage().contains("no such column")) {
return false;
}
}
return true;
}
/**
* Sets up upgrade.
*
* @param oldVersion thd old database version
*/
private void setupUpgrade(int oldVersion) {
dropTable(TracksColumns.TABLE_NAME);
dropTable(TrackPointsColumns.TABLE_NAME);
dropTable(WaypointsColumns.TABLE_NAME);
createTable(TracksColumns.TABLE_NAME);
createTable(TrackPointsColumns.TABLE_NAME);
createTable(WaypointsColumns.TABLE_NAME);
DatabaseHelper databaseHelper = new DatabaseHelper(context);
databaseHelper.onUpgrade(db, oldVersion, CustomContentProvider.DATABASE_VERSION);
}
}
@@ -28,7 +28,7 @@ import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -42,7 +42,7 @@ public class DescriptionGeneratorTest {
private static final long START_TIME = 1288721514000L;
private DescriptionGenerator descriptionGenerator;
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
@Before
public void setUp() {
@@ -55,7 +55,7 @@ public class DescriptionGeneratorTest {
@Test
public void testGenerateTrackDescription() {
Track track = new Track();
TripStatistics stats = new TripStatistics();
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalTime(600000);
stats.setMovingTime(300000);
@@ -63,10 +63,8 @@ public class DescriptionGeneratorTest {
stats.setMaxElevation(550);
stats.setMinElevation(-500);
stats.setTotalElevationGain(6000);
stats.setMaxGrade(0.42);
stats.setMinGrade(0.11);
stats.setStartTime(START_TIME);
track.setTripStatistics(stats);
stats.setStartTime_ms(START_TIME);
track.setTrackStatistics(stats);
track.setCategory("hiking");
String expected = //"Created by"
"<a href='https://github.com/OpenTracksApp/OpenTracks'>OpenTracks</a><p>"
@@ -85,8 +83,6 @@ public class DescriptionGeneratorTest {
+ "Max elevation: 550 m (1804 ft)<br>"
+ "Min elevation: -500 m (-1640 ft)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Max grade: 42 %<br>"
+ "Min grade: 11 %<br>"
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "<br>";
Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
@@ -142,34 +138,4 @@ public class DescriptionGeneratorTest {
descriptionGenerator.writePace(1.1, builder, R.string.description_average_pace_in_minute, "\n");
Assert.assertEquals("Average pace: 15:09 min/km (24:23 min/mi)\n", builder.toString());
}
/**
* Tests {@link DescriptionGenerator#writeGrade(double, StringBuilder, int, String)}.
*/
@Test
public void testWriteGrade() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeGrade(.042, builder, R.string.description_max_grade, "<br>");
Assert.assertEquals("Max grade: 4 %<br>", builder.toString());
}
/**
* Tests {@link DescriptionGenerator#writeGrade(double, StringBuilder, int, String)} with a NaN.
*/
@Test
public void testWriteGrade_nan() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeGrade(Double.NaN, builder, R.string.description_max_grade, "<br>");
Assert.assertEquals("Max grade: 0 %<br>", builder.toString());
}
/**
* Tests {@link DescriptionGenerator#writeGrade(double, StringBuilder, int, String)} with an infinite number.
*/
@Test
public void testWriteGrade_infinite() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeGrade(Double.POSITIVE_INFINITY, builder, R.string.description_max_grade, "<br>");
Assert.assertEquals("Max grade: 0 %<br>", builder.toString());
}
}
@@ -38,7 +38,8 @@ 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.stats.TripStatistics;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Tests for {@link SearchEngine}.
@@ -55,7 +56,7 @@ public class SearchEngineTest {
private ContentProviderUtils providerUtils;
private SearchEngine engine;
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
@Before
@@ -65,7 +66,7 @@ public class SearchEngineTest {
}
@After
public void tearDown() throws Exception {
public void tearDown() {
providerUtils.deleteAllTracks(context);
}
@@ -75,11 +76,11 @@ public class SearchEngineTest {
track.setDescription(description);
track.setCategory(category);
TripStatistics stats = track.getTripStatistics();
TrackStatistics stats = track.getTrackStatistics();
if (hoursAgo > 0) {
// Started twice hoursAgo, so the average time is hoursAgo.
stats.setStartTime(NOW - hoursAgo * 1000L * 60L * 60L * 2);
stats.setStopTime(NOW);
stats.setStartTime_ms(NOW - hoursAgo * 1000L * 60L * 60L * 2);
stats.setStopTime_ms(NOW);
}
Uri uri = providerUtils.insertTrack(track);
@@ -99,19 +100,17 @@ public class SearchEngineTest {
}
private long insertWaypoint(String title, String description, String category, double distance, long hoursAgo, long trackId) {
Waypoint waypoint = new Waypoint();
waypoint.setName(title);
waypoint.setDescription(description);
waypoint.setCategory(category);
waypoint.setTrackId(trackId);
Location location = new Location(HERE);
location.setLatitude(location.getLatitude() + distance);
location.setLongitude(location.getLongitude() + distance);
if (hoursAgo >= 0) {
location.setTime(NOW - hoursAgo * 1000L * 60L * 60L);
}
waypoint.setLocation(location);
Waypoint waypoint = new Waypoint(location);
waypoint.setName(title);
waypoint.setDescription(description);
waypoint.setCategory(category);
waypoint.setTrackId(trackId);
Uri uri = providerUtils.insertWaypoint(waypoint);
return ContentUris.parseId(uri);
@@ -152,7 +151,7 @@ public class SearchEngineTest {
long allMatchId = insertTrack("aa", "ba", "ca");
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Title > Description > Category.
assertTrackResults(results, allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId, categoryMatchId);
@@ -177,7 +176,7 @@ public class SearchEngineTest {
long allMatchId = insertWaypoint("aa", "ba", "ca");
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Title > Description > Category.
assertWaypointResults(results, allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId, categoryMatchId);
@@ -198,7 +197,7 @@ public class SearchEngineTest {
long trackId = insertTrack("aa", "bb", "cc");
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Title > Description > Category.
Assert.assertEquals(results.toString(), 3, results.size());
@@ -215,7 +214,7 @@ public class SearchEngineTest {
long farId = insertWaypoint("ac", 0.2);
SearchQuery query = new SearchQuery("a", HERE, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Distance order.
assertWaypointResults(results, nearId, farId, farFarAwayId);
@@ -228,7 +227,7 @@ public class SearchEngineTest {
long oldId = insertTrack("ac", 2);
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Reverse time order.
assertTrackResults(results, recentId, oldId, oldestId);
@@ -242,7 +241,7 @@ public class SearchEngineTest {
long oldId = insertWaypoint("ac", 1);
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Reverse time order.
assertWaypointResults(results, recentId, oldId, oldestId);
@@ -255,7 +254,7 @@ public class SearchEngineTest {
long otherId = insertTrack("aa", 1);
SearchQuery query = new SearchQuery("a", null, currentId, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Current track should be demoted.
assertTrackResults(results, otherId, currentId);
@@ -268,7 +267,7 @@ public class SearchEngineTest {
long currentId = insertWaypoint("ab", 1, 123);
SearchQuery query = new SearchQuery("a", null, 123, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Waypoint in current track should be promoted.
assertWaypointResults(results, currentId, otherId);
@@ -19,9 +19,8 @@ public class TestDataUtil {
Track track = new Track();
track.setId(id);
track.setName("Test: " + id);
track.setNumberOfPoints(numPoints);
for (int i = 0; i < numPoints; i++) {
track.addLocation(createLocation(i));
track.addTrackPoint(createTrackPoint(i));
}
return track;
}
@@ -32,13 +31,13 @@ public class TestDataUtil {
* @param i the index to set the value of location.
* @return created location
*/
public static Location createLocation(int i) {
Location loc = new Location("test");
loc.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0);
loc.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0);
loc.setAccuracy((float) i / 100.0f);
loc.setAltitude(i * ALTITUDE_INTERVAL);
loc.setTime(i + 1);
return loc;
public static TrackPoint createTrackPoint(int i) {
Location location = new Location("test");
location.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0);
location.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0);
location.setAccuracy((float) i / 100.0f);
location.setAltitude(i * ALTITUDE_INTERVAL);
location.setTime(i + 1);
return new TrackPoint(location);
}
}
@@ -0,0 +1,71 @@
/*
* 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.content.provider;
import android.content.ContentUris;
import android.content.Context;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
/**
* Tests {@link CustomContentProvider}.
*
* @author Youtao Liu
*/
public class CustomContentProviderTest {
private CustomContentProvider customContentProvider;
private final Context context = ApplicationProvider.getApplicationContext();
@Before
public void setUp() {
customContentProvider = new CustomContentProvider() {
};
}
/**
* Tests {@link CustomContentProvider#onCreate(android.content.Context)}.
*/
@Test
public void testOnCreate() {
Assert.assertTrue(customContentProvider.onCreate(context));
}
/**
* Tests {@link CustomContentProvider#getType(Uri)}.
*/
@Test
public void testGetType() {
Assert.assertEquals(TracksColumns.CONTENT_TYPE, customContentProvider.getType(TracksColumns.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()));
}
}
@@ -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;
@@ -33,15 +33,15 @@ import org.mockito.junit.MockitoJUnitRunner;
import java.util.ArrayList;
import java.util.List;
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;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.mockito.Mockito.when;
@@ -58,7 +58,7 @@ public class CustomContentProviderUtilsTest {
private static final String TEST_DESC = "Test Desc!";
private static final String TEST_DESC_NEW = "Test Desc new!";
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
@Mock
@@ -75,114 +75,86 @@ public class CustomContentProviderUtilsTest {
@Test
public void testLocationIterator_noPoints() {
testIterator(1, 0, 1, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_customFactory() {
final Location location = new Location("test_location");
final AtomicInteger counter = new AtomicInteger();
testIterator(1, 15, 4, false, new LocationFactory() {
@Override
public Location createLocation() {
counter.incrementAndGet();
return location;
}
});
// Make sure we were called exactly as many times as we had track points.
Assert.assertEquals(15, counter.get());
}
@Test
public void testLocationIterator_nullFactory() {
try {
testIterator(1, 15, 4, false, null);
Assert.fail("Expecting IllegalArgumentException");
} catch (IllegalArgumentException e) {
// Expected.
}
testIterator(1, 0, 1, false);
}
@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);
testIterator(2, 50, 50, false);
}
@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);
testIterator(2, 50, 50, true);
}
@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);
testIterator(2, 50, 25, false);
}
@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);
testIterator(2, 50, 25, true);
}
@Test
public void testLocationIterator_largeTrack() {
testIterator(1, 20000, 2000, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(1, 20000, 2000, false);
}
private List<Location> testIterator(long trackId, int numPoints, int batchSize, boolean descending, LocationFactory locationFactory) {
private void testIterator(long trackId, int numPoints, int batchSize, boolean descending) {
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)) {
contentProviderUtils.setDefaultCursorBatchSize(batchSize);
List<TrackPoint> locations = new ArrayList<>(numPoints);
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(trackId, -1L, descending)) {
while (it.hasNext()) {
Location loc = it.next();
TrackPoint 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());
}
return locations;
}
private long initializeTrack(long id, int numPoints) {
Track track = new Track();
track.setId(id);
track.setName("Test: " + id);
track.setNumberOfPoints(numPoints);
contentProviderUtils.insertTrack(track);
track = contentProviderUtils.getTrack(id);
Assert.assertNotNull(track);
Location[] locations = new Location[numPoints];
TrackPoint[] trackPoints = new TrackPoint[numPoints];
for (int i = 0; i < numPoints; ++i) {
Location loc = new Location("test");
loc.setLatitude(37.0 + (double) i / 10000.0);
loc.setLongitude(57.0 - (double) i / 10000.0);
loc.setAccuracy((float) i / 100.0f);
loc.setAltitude(i * 2.5);
locations[i] = loc;
trackPoints[i] = new TrackPoint(loc);
}
contentProviderUtils.bulkInsertTrackPoint(locations, numPoints, id);
contentProviderUtils.bulkInsertTrackPoint(trackPoints, numPoints, id);
// Load all inserted locations.
// Load all inserted trackPoints.
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)) {
while (it.hasNext()) {
it.next();
lastPointId = it.getLocationId();
lastPointId = it.getTrackPointId();
counter++;
}
}
Assert.assertTrue(numPoints == 0 || lastPointId > 0);
Assert.assertEquals(numPoints, track.getNumberOfPoints());
Assert.assertEquals(numPoints, counter);
return lastPointId;
@@ -229,7 +201,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 +210,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 +236,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 +245,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());
@@ -339,10 +311,9 @@ public class CustomContentProviderUtilsTest {
// Bottom
long startTime = 1000L;
// AverageSpeed
double minGrade = -20.11;
TripStatistics statistics = new TripStatistics();
statistics.setStartTime(startTime);
statistics.setStopTime(2500L);
TrackStatistics statistics = new TrackStatistics();
statistics.setStartTime_ms(startTime);
statistics.setStopTime_ms(2500L);
statistics.setTotalTime(1500L);
statistics.setMovingTime(700L);
statistics.setTotalDistance(750.0);
@@ -350,22 +321,19 @@ public class CustomContentProviderUtilsTest {
statistics.setMaxSpeed(60.0);
statistics.setMaxElevation(1250.0);
statistics.setMinElevation(1200.0);
statistics.setMaxGrade(15.0);
statistics.setMinGrade(minGrade);
track.setTripStatistics(statistics);
track.setTrackStatistics(statistics);
contentProviderUtils.insertTrack(track);
// Insert at first.
Waypoint waypoint = new Waypoint();
waypoint.setDescription(TEST_DESC);
Location location = new Location("test");
location.setLatitude(22);
location.setLongitude(22);
location.setAccuracy((float) 1 / 100.0f);
location.setAltitude(2.5);
waypoint.setLocation(location);
Waypoint waypoint = new Waypoint(location);
waypoint.setDescription(TEST_DESC);
contentProviderUtils.insertWaypoint(waypoint);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(contentResolverMock);
@@ -414,7 +382,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
@@ -430,14 +398,14 @@ 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
@@ -445,9 +413,9 @@ public class CustomContentProviderUtilsTest {
long trackId = System.currentTimeMillis();
Track track = TestDataUtil.getTrack(trackId, 10);
TripStatistics statistics = new TripStatistics();
statistics.setStartTime(1000L);
statistics.setStopTime(2500L);
TrackStatistics statistics = new TrackStatistics();
statistics.setStartTime_ms(1000L);
statistics.setStopTime_ms(2500L);
statistics.setTotalTime(1500L);
statistics.setMovingTime(700L);
statistics.setTotalDistance(750.0);
@@ -455,10 +423,8 @@ public class CustomContentProviderUtilsTest {
statistics.setMaxSpeed(60.0);
statistics.setMaxElevation(1250.0);
statistics.setMinElevation(1200.0);
statistics.setMaxGrade(15.0);
statistics.setMinGrade(-25.0);
track.setTripStatistics(statistics);
track.setTrackStatistics(statistics);
contentProviderUtils.insertTrack(track);
@@ -475,7 +441,7 @@ public class CustomContentProviderUtilsTest {
// 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());
@@ -506,32 +472,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)}.
@@ -573,7 +513,7 @@ public class CustomContentProviderUtilsTest {
}
/**
* Tests the method {@link ContentProviderUtils#bulkInsertTrackPoint(Location[], int, long)}.
* Tests the method {@link ContentProviderUtils#bulkInsertTrackPoint(TrackPoint[], int, long)}.
*/
@Test
public void testBulkInsertTrackPoint() {
@@ -582,9 +522,9 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
contentProviderUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), -1, trackId);
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), -1, trackId);
Assert.assertEquals(20, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
contentProviderUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), 8, trackId);
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), 8, trackId);
Assert.assertEquals(28, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
}
@@ -640,16 +580,16 @@ public class CustomContentProviderUtilsTest {
byte[] sensor = "Sensor state".getBytes();
when(cursorMock.getBlob(index++)).thenReturn(sensor);
Location location = contentProviderUtils.createTrackPoint(cursorMock);
Assert.assertEquals((double) longitude, location.getLongitude(), 0.01);
Assert.assertEquals((double) latitude, location.getLatitude(), 0.01);
TrackPoint location = contentProviderUtils.createTrackPoint(cursorMock);
Assert.assertEquals(longitude, location.getLongitude(), 0.01);
Assert.assertEquals(latitude, location.getLatitude(), 0.01);
Assert.assertEquals(time, location.getTime(), 0.01);
Assert.assertEquals(speed, location.getSpeed(), 0.01);
}
/**
* Tests the method
* {@link ContentProviderUtils#insertTrackPoint(Location, long)}.
* {@link ContentProviderUtils#insertTrackPoint(TrackPoint, long)}.
*/
@Test
public void testInsertTrackPoint() {
@@ -658,7 +598,7 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
contentProviderUtils.insertTrackPoint(TestDataUtil.createLocation(22), trackId);
contentProviderUtils.insertTrackPoint(TestDataUtil.createTrackPoint(22), trackId);
Assert.assertEquals(11, contentProviderUtils.getTrackPointCursor(trackId, -1L, 1000, false).getCount());
}
@@ -672,8 +612,8 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.getTrack(trackId, 10);
insertTrackWithLocations(track);
Location lastLocation = contentProviderUtils.getLastValidTrackPoint(trackId);
checkLocation(9, lastLocation);
TrackPoint lastTrackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
checkLocation(9, lastTrackPoint.getLocation());
}
/**
@@ -686,9 +626,9 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
long[] trackpointIds = new long[track.getNumberOfPoints()];
long[] trackpointIds = new long[track.getTrackPoints().size()];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId()));
}
Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds[1], 5, true);
@@ -705,9 +645,9 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
long[] trackpointIds = new long[track.getNumberOfPoints()];
long[] trackpointIds = new long[track.getTrackPoints().size()];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId()));
}
Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackpointIds[8], 5, false);
@@ -715,7 +655,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)} in descending.
*/
@Test
public void testGetTrackPointLocationIterator_desc() {
@@ -724,25 +664,25 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
long[] trackpointIds = new long[track.getNumberOfPoints()];
long[] trackpointIds = new long[track.getTrackPoints().size()];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId()));
}
long startTrackPointId = trackpointIds[9];
LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true);
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next();
Assert.assertEquals(startTrackPointId - i, locationIterator.getLocationId());
checkLocation((trackpointIds.length - 1) - i, location);
Assert.assertTrue(trackPointIterator.hasNext());
TrackPoint trackPoint = trackPointIterator.next();
Assert.assertEquals(startTrackPointId - i, trackPointIterator.getTrackPointId());
checkLocation((trackpointIds.length - 1) - i, trackPoint.getLocation());
}
Assert.assertFalse(locationIterator.hasNext());
Assert.assertFalse(trackPointIterator.hasNext());
}
/**
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in ascending.
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean)} in ascending.
*/
@Test
public void testGetTrackPointLocationIterator_asc() {
@@ -751,20 +691,20 @@ public class CustomContentProviderUtilsTest {
Track track = TestDataUtil.getTrack(trackId, 10);
contentProviderUtils.insertTrack(track);
long[] trackpointIds = new long[track.getNumberOfPoints()];
long[] trackpointIds = new long[track.getTrackPoints().size()];
for (int i = 0; i < trackpointIds.length; i++) {
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getTrackPoints().get(i), track.getId()));
}
long startTrackPointId = trackpointIds[0];
LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false);
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next();
Assert.assertEquals(startTrackPointId + i, locationIterator.getLocationId());
TrackPoint trackPoint = locationIterator.next();
Assert.assertEquals(startTrackPointId + i, locationIterator.getTrackPointId());
checkLocation(i, location);
checkLocation(i, trackPoint.getLocation());
}
Assert.assertFalse(locationIterator.hasNext());
}
@@ -789,6 +729,14 @@ public class CustomContentProviderUtilsTest {
*/
private void insertTrackWithLocations(Track track) {
contentProviderUtils.insertTrack(track);
contentProviderUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), track.getLocations().size(), track.getId());
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), track.getTrackPoints().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}));
}
}
@@ -0,0 +1,179 @@
package de.dennisguse.opentracks.content.provider;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.HashMap;
import java.util.Map;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
@RunWith(AndroidJUnit4.class)
public class CustomSQLiteOpenHelperTest {
private static final String TRACKS_CREATE_TABLE_V23 = "CREATE TABLE tracks (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, starttime INTEGER, stoptime INTEGER, numpoints INTEGER, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, mingrade FLOAT, maxgrade FLOAT, icon TEXT)";
private static final String TRACKPOINTS_CREATE_TABLE_V23 = "CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT)";
private static final String WAYPOINTS_CREATE_TABLE_V23 = "CREATE TABLE waypoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, icon TEXT, trackid INTEGER, type INTEGER, length FLOAT, duration INTEGER, starttime INTEGER, startid INTEGER, stopid INTEGER, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, mingrade FLOAT, maxgrade FLOAT, photoUrl TEXT)";
private static final String DATABASE_NAME = "test.db";
private final Context context = ApplicationProvider.getApplicationContext();
/**
* Get the SQL create statements for all tables (ordered by name).
*
* @return Map(TableName, SQL)
*/
private static Map<String, String> getTableSQL(SQLiteDatabase db) {
HashMap<String, String> tableSQL = new HashMap<>();
try (Cursor cursor = db.query("sqlite_master", new String[]{"name", "SQL"}, "name IN ('" + TracksColumns.TABLE_NAME + "', '" + TrackPointsColumns.TABLE_NAME + "', '" + WaypointsColumns.TABLE_NAME + "')", null, null, null, "name")) {
if (cursor != null) {
while (cursor.moveToNext()) {
tableSQL.put(cursor.getString(0), cursor.getString(1));
}
}
}
return tableSQL;
}
/**
* Get the SQL create statements for all indices (ordered by name).
*
* @return Map(tableName, SQL)
*/
public static Map<String, String> getIndexSQL(SQLiteDatabase db) {
HashMap<String, String> indexSQL = new HashMap<>();
try (Cursor cursor = db.rawQuery("SELECT tbl_name, SQL FROM sqlite_master WHERE type = 'index'", null)) {
if (cursor != null) {
while (cursor.moveToNext()) {
indexSQL.put(cursor.getString(0), cursor.getString(1));
}
}
}
return indexSQL;
}
/**
* Returns true if a SQL create statement is in the schema.
*
* @param sqlCreate the table name
*/
private static boolean hasSqlCreate(SQLiteDatabase db, String sqlCreate) {
try (Cursor cursor = db.rawQuery("SELECT SQL FROM sqlite_master", null)) {
if (cursor != null) {
while (cursor.moveToNext()) {
String sql = cursor.getString(0);
if (sqlCreate.equals(sql)) {
return true;
}
}
}
}
return false;
}
@Before
public void setUp() {
context.deleteDatabase(DATABASE_NAME);
}
@Test
public void onCreate() {
try (SQLiteDatabase db = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getWritableDatabase()) {
Assert.assertTrue(hasSqlCreate(db, TracksColumns.CREATE_TABLE));
Assert.assertTrue(hasSqlCreate(db, TrackPointsColumns.CREATE_TABLE));
Assert.assertTrue(hasSqlCreate(db, TrackPointsColumns.CREATE_TABLE_INDEX));
Assert.assertTrue(hasSqlCreate(db, WaypointsColumns.CREATE_TABLE));
Assert.assertTrue(hasSqlCreate(db, WaypointsColumns.CREATE_TABLE_INDEX));
}
}
@Test
public void onUpgrade_FromVersion23() {
// Manually create database schema with version 23 (base version)
{
SQLiteDatabase dbBase = new SQLiteOpenHelper(context, DATABASE_NAME, null, 23) {
@Override
public void onCreate(SQLiteDatabase db) {
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}.getWritableDatabase();
dbBase.execSQL(TRACKS_CREATE_TABLE_V23);
dbBase.execSQL(TRACKPOINTS_CREATE_TABLE_V23);
dbBase.execSQL(WAYPOINTS_CREATE_TABLE_V23);
dbBase.close();
}
// Open database with SQL upgrade
Map<String, String> tableByUpgrade;
Map<String, String> indicesByUpgrade;
try (SQLiteDatabase dbUpgraded = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase()) {
tableByUpgrade = getTableSQL(dbUpgraded);
indicesByUpgrade = getIndexSQL(dbUpgraded);
}
// Open database via
context.deleteDatabase(DATABASE_NAME);
Map<String, String> tablesByCreate;
Map<String, String> indicesByCreate;
try (SQLiteDatabase dbCreated = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase()) {
tablesByCreate = getTableSQL(dbCreated);
indicesByCreate = getIndexSQL(dbCreated);
}
// then - verify table structure
Assert.assertEquals(3, tableByUpgrade.size());
Assert.assertEquals(tableByUpgrade.size(), tablesByCreate.size());
Assert.assertEquals(tablesByCreate.get(TracksColumns.TABLE_NAME), tableByUpgrade.get(TracksColumns.TABLE_NAME));
Assert.assertEquals(tablesByCreate.get(TrackPointsColumns.TABLE_NAME), tableByUpgrade.get(TrackPointsColumns.TABLE_NAME));
Assert.assertEquals(tablesByCreate.get(WaypointsColumns.TABLE_NAME), tableByUpgrade.get(WaypointsColumns.TABLE_NAME));
// then - verify custom indices
Assert.assertEquals(2, indicesByCreate.size());
Assert.assertEquals(indicesByUpgrade.get(TrackPointsColumns.TABLE_NAME), indicesByCreate.get(TrackPointsColumns.TABLE_NAME));
Assert.assertEquals(indicesByUpgrade.get(WaypointsColumns.TABLE_NAME), indicesByCreate.get(WaypointsColumns.TABLE_NAME));
}
@Test
public void onDowngrade_ToVersion23() {
// Create most recent database schema
new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase().close();
// Downgrade schema to version 23 (base version)
Map<String, String> tablesByDowngrade;
Map<String, String> indicesByDowngrade;
try (SQLiteDatabase db = new CustomSQLiteOpenHelper(context, DATABASE_NAME, 23).getReadableDatabase()) {
tablesByDowngrade = getTableSQL(db);
indicesByDowngrade = getIndexSQL(db);
}
// then - verify table structure
Assert.assertEquals(TRACKS_CREATE_TABLE_V23, tablesByDowngrade.get("tracks"));
Assert.assertEquals(TRACKPOINTS_CREATE_TABLE_V23, tablesByDowngrade.get("trackpoints"));
Assert.assertEquals(WAYPOINTS_CREATE_TABLE_V23, tablesByDowngrade.get("waypoints"));
// then - verify custom indices
Assert.assertEquals(0, indicesByDowngrade.size());
}
}
@@ -0,0 +1,28 @@
package de.dennisguse.opentracks.content.provider;
import android.net.Uri;
import android.util.Pair;
import org.junit.Assert;
import org.junit.Test;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
public class ShareContentProviderTest {
@Test
public void testCreateandParseURI() {
long[] trackIds = {1, 3, 5};
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "TrackName", TrackFileFormat.KML_ONLY_TRACK);
Assert.assertArrayEquals(trackIds, ShareContentProvider.parseURI(shareURIandMIME.first));
}
@Test
public void testCreateURIescapeFilename() {
long[] trackIds = {1};
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "../../&1=1", TrackFileFormat.KML_ONLY_TRACK);
Assert.assertEquals(Uri.parse("content://de.dennisguse.opentracks.debug.content/tracks/kml_only_track/1/..%2F..%2F%261%3D1.kml"), shareURIandMIME.first);
}
}
@@ -30,6 +30,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.TrackStubUtils;
import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
@@ -61,290 +62,271 @@ 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);
chartFragment.setTrackStatisticsUpdater(TrackStubUtils.INITIAL_TIME);
}
/**
* Tests the logic to get the incorrect values of sensor in {@link ChartFragment#fillDataPoint(Location, double[])}.
* Tests the logic to get the incorrect values of sensor in {@link ChartFragment#createPendingPoint(TrackPoint)}.
*/
@Test
public void testFillDataPoint_sensorIncorrect() {
TrackPoint sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
public void testCreatePendingPoint_sensorIncorrect() {
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
// No input.
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(Float.NaN, point[ChartView.HEART_RATE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.CADENCE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.POWER_SERIES + 1], 0.01);
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01);
Assert.assertEquals(Float.NaN, point.getCadence(), 0.01);
Assert.assertEquals(Float.NaN, point.getPower(), 0.01);
// Input incorrect state.
// Creates SensorData.
SensorDataSet sensorDataSet = new SensorDataSet(SensorDataSet.DATA_UNAVAILABLE, SensorDataSet.DATA_UNAVAILABLE);
sensorDataSetLocation.setSensorDataSet(sensorDataSet);
trackPoint.setSensorDataSet(sensorDataSet);
// Test.
point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(Float.NaN, point[ChartView.HEART_RATE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.CADENCE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.POWER_SERIES + 1], 0.01);
point = chartFragment.createPendingPoint(trackPoint);
Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01);
Assert.assertEquals(Float.NaN, point.getCadence(), 0.01);
Assert.assertEquals(Float.NaN, point.getPower(), 0.01);
}
/**
* Tests the logic to get the correct values of sensor in {@link ChartFragment#fillDataPoint(Location, double[])}.
* Tests the logic to get the correct values of sensor in {@link ChartFragment#createPendingPoint(TrackPoint)}.
*/
@Test
public void testFillDataPoint_sensorCorrect() {
TrackPoint sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
public void testCreatePendingPoint_sensorCorrect() {
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
// No input.
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(Float.NaN, point[ChartView.HEART_RATE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.CADENCE_SERIES + 1], 0.01);
Assert.assertEquals(Float.NaN, point[ChartView.POWER_SERIES + 1], 0.01);
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01);
Assert.assertEquals(Float.NaN, point.getCadence(), 0.01);
Assert.assertEquals(Float.NaN, point.getPower(), 0.01);
// Creates SensorData.
SensorDataSet sensorDataSet = new SensorDataSet(100, 101, 102);
// Creates SensorDataSet.
sensorDataSetLocation.setSensorDataSet(sensorDataSet);
trackPoint.setSensorDataSet(sensorDataSet);
// Test.
point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(100.0, point[ChartView.HEART_RATE_SERIES + 1], 0.01);
Assert.assertEquals(101.0, point[ChartView.CADENCE_SERIES + 1], 0.01);
Assert.assertEquals(102.0, point[ChartView.POWER_SERIES + 1], 0.01);
point = chartFragment.createPendingPoint(trackPoint);
Assert.assertEquals(100.0, point.getHeartRate(), 0.01);
Assert.assertEquals(101.0, point.getCadence(), 0.01);
Assert.assertEquals(102.0, point.getPower(), 0.01);
}
/**
* Tests the logic to get the value of metric Distance in {@link ChartFragment#fillDataPoint(Location, double[])}.
* Tests the logic to get the value of metric Distance in {@link ChartFragment#createPendingPoint(TrackPoint)}.
*/
@Test
public void testFillDataPoint_distanceMetric() {
// By distance.
public void testCreatePendingPoint_distanceMetric() {
chartFragment.setChartByDistance(true);
// Resets last location and writes first location.
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0], 0.01);
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01);
// The second is a same location, just different time.
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(0.0, point[0], 0.01);
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
point = chartFragment.createPendingPoint(trackPoint2);
Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01);
// The third location is a new location, and use metric.
TrackPoint sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation3.setLatitude(23);
point = fillDataPointTestHelper(sensorDataSetLocation3);
TrackPoint trackPoint3 = TrackStubUtils.createDefaultTrackPoint();
trackPoint3.setLatitude(23);
point = chartFragment.createPendingPoint(trackPoint3);
// Computes the distance between Latitude 22 and 23.
float[] results = new float[4];
Location.distanceBetween(sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(),
sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(), results);
Location.distanceBetween(trackPoint2.getLatitude(), trackPoint2.getLongitude(),
trackPoint3.getLatitude(), trackPoint3.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM;
Assert.assertEquals(distance1, point[0], 0.01);
Assert.assertEquals(distance1, point.getTimeOrDistance(), 0.01);
// The fourth location is a new location, and use metric.
TrackPoint sensorDataSetLocation4 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation4.setLatitude(24);
point = fillDataPointTestHelper(sensorDataSetLocation4);
TrackPoint trackPoint4 = TrackStubUtils.createDefaultTrackPoint();
trackPoint4.setLatitude(24);
point = chartFragment.createPendingPoint(trackPoint4);
// Computes the distance between Latitude 23 and 24.
Location.distanceBetween(sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(),
sensorDataSetLocation4.getLatitude(), sensorDataSetLocation4.getLongitude(), results);
Location.distanceBetween(trackPoint3.getLatitude(), trackPoint3.getLongitude(),
trackPoint4.getLatitude(), trackPoint4.getLongitude(), results);
double distance2 = results[0] * UnitConversions.M_TO_KM;
Assert.assertEquals((distance1 + distance2), point[0], 0.01);
Assert.assertEquals((distance1 + distance2), point.getTimeOrDistance(), 0.01);
}
/**
* Tests the logic to get the value of imperial Distance in {@link ChartFragment#fillDataPoint(Location, double[])}.
* Tests the logic to get the value of imperial Distance in {@link ChartFragment#createPendingPoint(TrackPoint)}.
*/
@Test
public void testFillDataPoint_distanceImperial() {
public void testCreatePendingPoint_distanceImperial() {
// By distance.
chartFragment.setChartByDistance(true);
// Setups to use imperial.
chartFragment.setMetricUnits(false);
// The first is a same location, just different time.
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0], 0.01);
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01);
// The second location is a new location, and use imperial.
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setLatitude(23);
point = fillDataPointTestHelper(sensorDataSetLocation2);
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
trackPoint2.setLatitude(23);
point = chartFragment.createPendingPoint(trackPoint2);
/*
* Computes the distance between Latitude 22 and 23.
* And for we set using * imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
*/
float[] results = new float[4];
Location.distanceBetween(sensorDataSetLocation1.getLatitude(), sensorDataSetLocation1.getLongitude(), sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), results);
Location.distanceBetween(trackPoint1.getLatitude(), trackPoint1.getLongitude(), trackPoint2.getLatitude(), trackPoint2.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
Assert.assertEquals(distance1, point[0], 0.01);
Assert.assertEquals(distance1, point.getTimeOrDistance(), 0.01);
// The third location is a new location, and use imperial.
TrackPoint sensorDataSetLocation3 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation3.setLatitude(24);
point = fillDataPointTestHelper(sensorDataSetLocation3);
TrackPoint trackPoint3 = TrackStubUtils.createDefaultTrackPoint();
trackPoint3.setLatitude(24);
point = chartFragment.createPendingPoint(trackPoint3);
/*
* Computes the distance between Latitude 23 and 24.
* And for we set using * imperial, the distance should be multiplied by UnitConversions.KM_TO_MI.
*/
Location.distanceBetween(sensorDataSetLocation2.getLatitude(), sensorDataSetLocation2.getLongitude(), sensorDataSetLocation3.getLatitude(), sensorDataSetLocation3.getLongitude(), results);
Location.distanceBetween(trackPoint2.getLatitude(), trackPoint2.getLongitude(), trackPoint3.getLatitude(), trackPoint3.getLongitude(), results);
double distance2 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
Assert.assertEquals(distance1 + distance2, point[0], 0.01);
Assert.assertEquals(distance1 + distance2, point.getTimeOrDistance(), 0.01);
}
/**
* Tests the logic to get the values of time in {@link ChartFragment#fillDataPoint(Location, double[])}.
* Tests the logic to get the values of time in {@link ChartFragment#createPendingPoint(TrackPoint)}.
*/
@Test
public void testFillDataPoint_time() {
// By time
public void testCreatePendingPoint_time() {
chartFragment.setChartByDistance(false);
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[0], 0.01);
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01);
long timeSpan = 222;
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + timeSpan);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals((double) timeSpan, point[0], 0.01);
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
trackPoint2.setTime(trackPoint1.getTime() + timeSpan);
point = chartFragment.createPendingPoint(trackPoint2);
Assert.assertEquals((double) timeSpan, point.getTimeOrDistance(), 0.01);
}
/**
* Tests the logic to get the value of elevation in {@link ChartFragment#fillDataPoint(Location, double[])} by one and two points.
* Tests the logic to get the value of elevation in {@link ChartFragment#createPendingPoint(TrackPoint)} by one and two points.
*/
@Test
public void testFillDataPoint_elevation() {
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
public void testCreatePendingPoint_elevation() {
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
/*
* At first, clear old points of elevation, so give true to the second parameter.
* Then only one value INITIAL_ALTITUDE in buffer.
*/
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point[ChartView.ELEVATION_SERIES + 1], 0.01);
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
Assert.assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point.getElevation(), 0.01);
/*
* Send another value to buffer, now there are two values, INITIAL_ALTITUDE and INITIAL_ALTITUDE * 2.
*/
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0,
point[ChartView.ELEVATION_SERIES + 1], 0.01);
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
trackPoint2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2);
point = chartFragment.createPendingPoint(trackPoint2);
Assert.assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0, point.getElevation(), 0.01);
}
/**
* Tests the logic to get the value of speed in {@link ChartFragment#fillDataPoint(Location, double[])}.
* Tests the logic to get the value of speed in {@link ChartFragment#createPendingPoint(TrackPoint)}.
* In this test, firstly remove all points in memory, and then fill in two points one by one.
* The speed values of these points are 129, 130.
*/
@Test
public void testFillDataPoint_speed() {
public void testCreatePendingPoint_speed() {
/*
* At first, clear old points of speed, so give true to the second parameter.
* It will not be filled in to the speed buffer.
*/
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(128.5f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(128.5f);
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
Assert.assertEquals(0.0, point.getSpeed(), 0.01);
/*
* Tests the logic when both metricUnits and reportSpeed are true.
* This location will be filled into speed buffer.
*/
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
/*
* Add a time span here to make sure the second point is valid, the value 222 here is doesn't matter.
*/
sensorDataSetLocation2.setTime(sensorDataSetLocation1.getTime() + 222);
sensorDataSetLocation2.setSpeed(130);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(130.0 * UnitConversions.MS_TO_KMH, point[ChartView.SPEED_SERIES + 1], 0.01);
trackPoint2.setTime(trackPoint1.getTime() + 222);
trackPoint2.setSpeed(130);
point = chartFragment.createPendingPoint(trackPoint2);
Assert.assertEquals(130.0 * UnitConversions.MS_TO_KMH, point.getSpeed(), 0.01);
}
/**
* Tests the logic to compute speed when use Imperial.
*/
@Test
public void testFillDataPoint_speedImperial() {
// Setups to use imperial.
public void testCreatePendingPoint_speedImperial() {
chartFragment.setMetricUnits(false);
// First data point is not added to the speed buffer
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(100.0f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(100.0f);
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
Assert.assertEquals(0.0, point.getSpeed(), 0.01);
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
/*
* Add a time span here to make sure the second point and the speed is valid.
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
*/
sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222);
sensorDataSetLocation2.setSpeed(102);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(102.0 * UnitConversions.MS_TO_KMH * UnitConversions.KM_TO_MI, point[ChartView.SPEED_SERIES + 1], 0.01);
trackPoint2.setTime(trackPoint2.getTime() + 222);
trackPoint2.setSpeed(102);
point = chartFragment.createPendingPoint(trackPoint2);
Assert.assertEquals(102.0 * UnitConversions.MS_TO_KMH * UnitConversions.KM_TO_MI, point.getSpeed(), 0.01);
}
/**
* Tests the logic to get pace value when reportSpeed is false.
*/
@Test
public void testFillDataPoint_pace_nonZeroSpeed() {
// Setups reportSpeed to false.
public void testCreatePendingPoint_pace_nonZeroSpeed() {
chartFragment.setReportSpeed(false);
// First data point is not added to the speed buffer
TrackPoint sensorDataSetLocation1 = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation1.setSpeed(100.0f);
double[] point = fillDataPointTestHelper(sensorDataSetLocation1);
Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01);
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(100.0f);
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
Assert.assertEquals(0.0, point.getSpeed(), 0.01);
TrackPoint sensorDataSetLocation2 = TrackStubUtils.createSensorDataSetLocation();
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
/*
* Add a time span here to make sure the second point and the speed is valid.
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
*/
sensorDataSetLocation2.setTime(sensorDataSetLocation2.getTime() + 222);
sensorDataSetLocation2.setSpeed(102);
point = fillDataPointTestHelper(sensorDataSetLocation2);
Assert.assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MS_TO_KMH), point[ChartView.PACE_SERIES + 1], 0.01);
trackPoint2.setTime(trackPoint2.getTime() + 222);
trackPoint2.setSpeed(102);
point = chartFragment.createPendingPoint(trackPoint2);
Assert.assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MS_TO_KMH), point.getPace(), 0.01);
}
/**
* Tests the logic to get pace value when reportSpeed is false and average speed is zero.
*/
@Test
public void testFillDataPoint_pace_zeroSpeed() {
// Setups reportSpeed to false.
public void testCreatePendingPoint_pace_zeroSpeed() {
chartFragment.setReportSpeed(false);
TrackPoint sensorDataSetLocation = TrackStubUtils.createSensorDataSetLocation();
sensorDataSetLocation.setSpeed(0);
double[] point = fillDataPointTestHelper(sensorDataSetLocation);
Assert.assertEquals(0.0, point[ChartView.PACE_SERIES + 1], 0.01);
}
/**
* Helper method to test fillDataPoint.
*
* @param location location to fill
* @return data of this location
*/
private double[] fillDataPointTestHelper(Location location) {
double[] point = new double[ChartView.NUM_SERIES + 1];
chartFragment.fillDataPoint(location, point);
return point;
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
trackPoint.setSpeed(0);
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
Assert.assertEquals(0.0, point.getPace(), 0.01);
}
}
@@ -32,11 +32,12 @@ 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.TrackPoint;
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 +58,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);
@@ -83,24 +84,24 @@ public abstract class AbstractTestFileTrackImporter {
@Mock
public ContentProviderUtils contentProviderUtils;
Location createLocation(int index, long time) {
Location location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(TRACK_LATITUDE + index);
location.setLongitude(TRACK_LONGITUDE + index);
location.setAltitude(TRACK_ELEVATION + index);
location.setTime(time);
return location;
TrackPoint createTrackPoint(int index, long time) {
Location trackPoint = new Location(LocationManager.GPS_PROVIDER);
trackPoint.setLatitude(TRACK_LATITUDE + index);
trackPoint.setLongitude(TRACK_LONGITUDE + index);
trackPoint.setAltitude(TRACK_ELEVATION + index);
trackPoint.setTime(time);
return new TrackPoint(trackPoint);
}
/**
* Expects the first track point to be added.
*
* @param location the location
* @param trackPoint the trackPoint
* @param trackId the track id
* @param trackPointId the track point id
*/
protected void expectFirstTrackPoint(Location location, long trackId, long trackPointId) {
when(contentProviderUtils.bulkInsertTrackPoint(location != null ? (Location[]) any() : (Location[]) any(), eq(1), eq(trackId))).thenReturn(1);
protected void expectFirstTrackPoint(TrackPoint trackPoint, long trackId, long trackPointId) {
when(contentProviderUtils.bulkInsertTrackPoint(trackPoint != null ? (TrackPoint[]) any() : (TrackPoint[]) any(), eq(1), eq(trackId))).thenReturn(1);
}
/**
@@ -124,7 +125,7 @@ public abstract class AbstractTestFileTrackImporter {
Assert.assertEquals(name, track.getName());
Assert.assertEquals(description, track.getDescription());
if (time != -1L) {
Assert.assertEquals(time, track.getTripStatistics().getStartTime());
Assert.assertEquals(time, track.getTrackStatistics().getStartTime_ms());
}
}
}
@@ -1,7 +1,6 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.location.Location;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider;
@@ -19,12 +18,14 @@ 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.TrackPoint;
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;
import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -32,23 +33,23 @@ import static org.junit.Assert.assertNotNull;
/**
* Export a track to {@link TrackFileFormat} and verify that the import is identical.
* <p>
* TODO: test ignores {@link de.dennisguse.opentracks.stats.TripStatistics} for now.
* TODO: test ignores {@link TrackStatistics} for now.
*/
@RunWith(JUnit4.class)
public class ExportImportTest {
private static final String TAG = ExportImportTest.class.getSimpleName();
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
private static final String TRACK_ICON = "the track icon";
private static final String TRACK_CATEGORY = "the category";
private static final String TRACK_DESCRIPTION = "the description";
private final List<Waypoint> waypoints = new ArrayList<>();
private long importTrackId;
private long trackId = System.currentTimeMillis();
private final long trackId = System.currentTimeMillis();
@Before
public void setUp() {
@@ -57,17 +58,16 @@ public class ExportImportTest {
track.setCategory(TRACK_CATEGORY);
track.setDescription(TRACK_DESCRIPTION);
contentProviderUtils.insertTrack(track);
contentProviderUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), track.getLocations().size(), track.getId());
contentProviderUtils.bulkInsertTrackPoint(track.getTrackPoints().toArray(new TrackPoint[0]), track.getTrackPoints().size(), track.getId());
for (int i = 0; i < 3; i++) {
Waypoint waypoint = new Waypoint();
Waypoint waypoint = new Waypoint(track.getTrackPoints().get(i).getLocation());
waypoint.setName("the waypoint " + i);
waypoint.setDescription("the waypoint description " + i);
waypoint.setCategory("the waypoint category" + i);
waypoint.setIcon("the waypoing icon" + i);
waypoint.setPhotoUrl("the photo url" + i);
waypoint.setTrackId(trackId);
waypoint.setLocation(track.getLocations().get(i));
contentProviderUtils.insertWaypoint(waypoint);
waypoints.add(waypoint);
@@ -114,7 +114,7 @@ public class ExportImportTest {
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals(track.getLocations(), importedTrack.getLocations());
assertEquals(track.getTrackPoints(), importedTrack.getTrackPoints());
assertEquals(track.getCategory(), importedTrack.getCategory());
assertEquals(track.getDescription(), importedTrack.getDescription());
assertEquals(track.getName(), importedTrack.getName());
@@ -184,7 +184,7 @@ public class ExportImportTest {
// 1. track
Track trackImported = contentProviderUtils.getTrack(importTrackId);
assertNotNull(trackImported);
assertEquals(track.getLocations(), trackImported.getLocations());
assertEquals(track.getTrackPoints(), trackImported.getTrackPoints());
assertEquals(track.getCategory(), trackImported.getCategory());
assertEquals(track.getDescription(), trackImported.getDescription());
assertEquals(track.getName(), trackImported.getName());
@@ -16,8 +16,6 @@
package de.dennisguse.opentracks.io.file.importer;
import android.location.Location;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -28,6 +26,7 @@ import java.io.ByteArrayInputStream;
import java.io.InputStream;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any;
@@ -74,9 +73,9 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
String longitude = Double.toString(TRACK_LONGITUDE + index);
String elevation = Double.toString(TRACK_ELEVATION + index);
StringBuilder buffer = new StringBuilder();
buffer.append("<trkpt lat=\"" + latitude + "\" lon=\"" + longitude + "\"><ele>" + elevation + "</ele>");
buffer.append("<trkpt lat=\"").append(latitude).append("\" lon=\"").append(longitude).append("\"><ele>").append(elevation).append("</ele>");
if (time != null) {
buffer.append("<time>" + time + "</time>");
buffer.append("<time>").append(time).append("</time>");
}
buffer.append("</trkpt>");
return buffer.toString();
@@ -85,14 +84,14 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
@Test
public void testOneTrackOneSegment() throws Exception {
// given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
TrackPoint trackPoint0 = createTrackPoint(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
TrackPoint trackPoint1 = createTrackPoint(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
ArgumentCaptor<Track> trackCaptor = ArgumentCaptor.forClass(Track.class);
@@ -109,19 +108,19 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime());
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTrackStatistics().getTotalTime());
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
}
@Test
public void testOneTrackTwoSegments() throws Exception {
// given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
TrackPoint trackPoint0 = createTrackPoint(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -141,7 +140,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
long time2 = DATE_FORMAT_1.parse(TRACK_TIME_2).getTime();
long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime();
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime());
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTrackStatistics().getTotalTime());
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
}
@@ -153,7 +152,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
expectFirstTrackPoint(null, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -169,7 +168,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
verify(contentProviderUtils, atLeastOnce()).updateTrack(trackCaptor.capture());
Assert.assertEquals(TRACK_ID_0, trackId);
Assert.assertEquals(0, trackCaptor.getValue().getTripStatistics().getTotalTime());
Assert.assertEquals(0, trackCaptor.getValue().getTrackStatistics().getTotalTime());
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, -1L);
}
@@ -207,7 +206,7 @@ public class GpxFileTrackImporterTest extends AbstractTestFileTrackImporter {
when(contentProviderUtils.insertTrack((Track) any())).thenReturn(TRACK_ID_0_URI);
// For the following, use StubReturn since we don't care whether they are invoked or not.
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), anyInt(), anyLong())).thenReturn(1);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), anyInt(), anyLong())).thenReturn(1);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
contentProviderUtils.deleteTrack(context, TRACK_ID_0);
@@ -16,8 +16,6 @@
package de.dennisguse.opentracks.io.file.importer;
import android.location.Location;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -28,6 +26,7 @@ import java.io.ByteArrayInputStream;
import java.io.InputStream;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.mockito.Mockito.any;
@@ -69,14 +68,14 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
@Test
public void testOneTrackOneSegment() throws Exception {
// given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
Location location1 = createLocation(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
TrackPoint trackPoint0 = createTrackPoint(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
TrackPoint trackPointl1 = createTrackPoint(1, DATE_FORMAT_1.parse(TRACK_TIME_1).getTime());
contentProviderUtils.clearTrack(context, TRACK_ID_0);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(1), eq(TRACK_ID_0))).thenReturn(1);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_1);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -94,20 +93,20 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
long time0 = DATE_FORMAT_0.parse(TRACK_TIME_0).getTime();
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTripStatistics().getTotalTime());
Assert.assertEquals(time1 - time0, trackCaptor.getValue().getTrackStatistics().getTotalTime());
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, time0);
}
@Test
public void testOneTrackTwoSegments() throws Exception {
// given
Location location0 = createLocation(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
TrackPoint trackPoint0 = createTrackPoint(0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
contentProviderUtils.clearTrack(context, TRACK_ID_0);
expectFirstTrackPoint(location0, TRACK_ID_0, TRACK_POINT_ID_0);
expectFirstTrackPoint(trackPoint0, TRACK_ID_0, TRACK_POINT_ID_0);
// A flush happens at the end
when(contentProviderUtils.bulkInsertTrackPoint((Location[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.bulkInsertTrackPoint((TrackPoint[]) any(), eq(5), eq(TRACK_ID_0))).thenReturn(5);
when(contentProviderUtils.getLastTrackPointId(TRACK_ID_0)).thenReturn(TRACK_POINT_ID_3);
when(contentProviderUtils.getTrack(PreferencesUtils.getRecordingTrackId(context))).thenReturn(null);
@@ -127,7 +126,7 @@ public class KmlFileTrackImporterTest extends AbstractTestFileTrackImporter {
long time1 = DATE_FORMAT_1.parse(TRACK_TIME_1).getTime();
long time2 = DATE_FORMAT_1.parse(TRACK_TIME_2).getTime();
long time3 = DATE_FORMAT_1.parse(TRACK_TIME_3).getTime();
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTripStatistics().getTotalTime());
Assert.assertEquals(time1 - time0 + time3 - time2, trackCaptor.getValue().getTrackStatistics().getTotalTime());
verifyTrack(trackCaptor.getValue(), TRACK_NAME_0, TRACK_DESCRIPTION_0, DATE_FORMAT_0.parse(TRACK_TIME_0).getTime());
}
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.services;
import android.app.NotificationManager;
import android.content.Context;
import android.location.Location;
import androidx.core.app.NotificationCompat;
import androidx.test.core.app.ApplicationProvider;
@@ -12,6 +11,8 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.content.data.TrackPoint;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
@@ -21,10 +22,10 @@ import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class TrackRecordingServiceNotificationManagerTest {
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
@Mock
private Location locationMock;
private TrackPoint locationMock;
@Mock
private NotificationCompat.Builder notificationCompatBuilder;
@@ -42,10 +43,10 @@ public class TrackRecordingServiceNotificationManagerTest {
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
// when
subject.updateLocation(context, locationMock, 100);
subject.updateLocation(context, locationMock, 100);
subject.updateLocation(context, locationMock, 1000);
subject.updateLocation(context, locationMock, 100);
subject.updateTrackPoint(context, locationMock, 100);
subject.updateTrackPoint(context, locationMock, 100);
subject.updateTrackPoint(context, locationMock, 1000);
subject.updateTrackPoint(context, locationMock, 100);
// then
verify(notificationCompatBuilder, times(6)).setOnlyAlertOnce(true);
@@ -42,11 +42,11 @@ 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.stats.TripStatistics;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
@@ -66,7 +66,7 @@ public class TrackRecordingServiceTest {
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils contentProviderUtils;
private final long trackId = Math.abs(new Random().nextLong());
@@ -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.
@@ -193,9 +193,9 @@ public class TrackRecordingServiceTest {
Track dummyTrack = new Track();
dummyTrack.setId(id);
dummyTrack.setName("Dummy Track");
TripStatistics tripStatistics = new TripStatistics();
tripStatistics.setStopTime(stopTime);
dummyTrack.setTripStatistics(tripStatistics);
TrackStatistics trackStatistics = new TrackStatistics();
trackStatistics.setStopTime_ms(stopTime);
dummyTrack.setTrackStatistics(trackStatistics);
addTrack(dummyTrack, isRecording);
}
@@ -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);
@@ -218,7 +218,7 @@ public class TrackRecordingServiceTest {
location.setSpeed(10);
location.setTime(System.currentTimeMillis());
location.setBearing(3.0f);
trackRecordingService.insertTrackPoint(location);
trackRecordingService.insertLocation(location);
Thread.sleep(200);
}
@@ -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);
@@ -25,10 +25,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.stats.TripStatistics;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
@@ -45,9 +45,9 @@ public class TrackRecordingServiceTestLooper {
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
private Context context = ApplicationProvider.getApplicationContext();
private final 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));
@@ -215,7 +215,7 @@ public class TrackRecordingServiceTestLooper {
location.setSpeed(10);
location.setTime(startTime + i * 10000);
location.setBearing(3.0f);
service.insertTrackPoint(location);
service.insertLocation(location);
if (i % 7 == 0) {
service.insertWaypoint(null, null, null, null);
@@ -226,12 +226,12 @@ 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();
Assert.assertNotNull(tripStatistics);
Assert.assertTrue(tripStatistics.getStartTime() > 0);
Assert.assertTrue(tripStatistics.getStopTime() >= tripStatistics.getStartTime());
TrackStatistics trackStatistics = track.getTrackStatistics();
Assert.assertNotNull(trackStatistics);
Assert.assertTrue(trackStatistics.getStartTime_ms() > 0);
Assert.assertTrue(trackStatistics.getStopTime_ms() >= trackStatistics.getStartTime_ms());
}
}
@@ -36,7 +36,7 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class AnnouncementPeriodicTaskFactoryTest {
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
@BeforeClass
public static void preSetUp() {
@@ -22,28 +22,28 @@ import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Tests for {@link TripStatistics}.
* Tests for {@link TrackStatistics}.
* This only tests non-trivial pieces of that class.
*
* @author Rodrigo Damazio
*/
@RunWith(JUnit4.class)
public class TripStatisticsTest {
public class TrackStatisticsTest {
private TripStatistics statistics;
private TrackStatistics statistics;
@Before
public void setUp() {
statistics = new TripStatistics();
statistics = new TrackStatistics();
}
@Test
public void testMerge() {
TripStatistics statistics2 = new TripStatistics();
statistics.setStartTime(1000L); // Resulting start time
statistics.setStopTime(2500L);
statistics2.setStartTime(3000L);
statistics2.setStopTime(4000L); // Resulting stop time
TrackStatistics statistics2 = new TrackStatistics();
statistics.setStartTime_ms(1000L); // Resulting start time
statistics.setStopTime_ms(2500L);
statistics2.setStartTime_ms(3000L);
statistics2.setStopTime_ms(4000L); // Resulting stop time
statistics.setTotalTime(1500L);
statistics2.setTotalTime(1000L); // Result: 1500+1000
statistics.setMovingTime(700L);
@@ -58,15 +58,11 @@ public class TripStatisticsTest {
statistics.setMinElevation(1200.0); // Resulting min elevation
statistics2.setMaxElevation(3575.0); // Resulting max elevation
statistics2.setMinElevation(2800.0);
statistics.setMaxGrade(15.0);
statistics.setMinGrade(-25.0); // Resulting min grade
statistics2.setMaxGrade(35.0); // Resulting max grade
statistics2.setMinGrade(0.0);
statistics.merge(statistics2);
Assert.assertEquals(1000L, statistics.getStartTime());
Assert.assertEquals(4000L, statistics.getStopTime());
Assert.assertEquals(1000L, statistics.getStartTime_ms());
Assert.assertEquals(4000L, statistics.getStopTime_ms());
Assert.assertEquals(2500L, statistics.getTotalTime());
Assert.assertEquals(1300L, statistics.getMovingTime());
Assert.assertEquals(1100.0, statistics.getTotalDistance(), 0.001);
@@ -74,8 +70,6 @@ public class TripStatisticsTest {
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
@@ -0,0 +1,34 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.stats.TrackStatistics;
@RunWith(AndroidJUnit4.class)
public class AnnouncementUtilsTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Test
public void getAnnouncement() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalTime(600000);
stats.setMovingTime(300000);
stats.setMaxSpeed(100);
stats.setTotalElevationGain(6000);
// when
String announcement = AnnouncementUtils.getAnnouncement(context, stats);
// then
Assert.assertEquals("OpenTracks total distance 20.00 kilometers in 5 minutes 0 seconds at 240.0 kilometers per hour", announcement);
}
}
@@ -21,6 +21,7 @@ import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.File;
import java.util.Arrays;
/**
* Tests for {@link FileUtils}.
@@ -108,9 +109,7 @@ public class FileUtilsTest {
File directory = new File("/dir1/dir2/");
String suffix = ".gpx";
char[] name = new char[FileUtils.MAX_FAT32_PATH_LENGTH];
for (int i = 0; i < name.length; i++) {
name[i] = 'a';
}
Arrays.fill(name, 'a');
String nameString = new String(name);
String truncated = FileUtils.truncateFileName(directory, nameString, suffix);
@@ -36,7 +36,7 @@ import java.util.TimeZone;
@RunWith(AndroidJUnit4.class)
public class StringUtilsTest {
private Context context = ApplicationProvider.getApplicationContext();
private final Context context = ApplicationProvider.getApplicationContext();
/**
@@ -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);
}
/**
+5 -5
View File
@@ -15,9 +15,7 @@ limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.dennisguse.opentracks"
android:installLocation="auto"
android:versionCode="3251"
android:versionName="v3.3.1"> <!-- For FDroid. -->
android:installLocation="auto">
<!-- Permission to initialize services -->
<application
android:name=".Startup"
@@ -31,7 +29,9 @@ limitations under the License.
<!-- Activities -->
<activity android:name=".AboutActivity" />
<activity android:name=".TrackDeleteActivity" />
<activity
android:name=".TrackDeleteActivity"
android:theme="@style/ThemeCustomTransparent" />
<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" />
@@ -19,14 +19,14 @@ 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.
*
* @author Jimmy Shih
*/
public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private final long[] trackIds;
private final Context context;
@@ -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;
@@ -16,18 +16,38 @@
package de.dennisguse.opentracks;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.util.Log;
import android.util.Pair;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
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;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.IntentUtils;
/**
* An activity to add/edit a marker.
@@ -39,16 +59,27 @@ public class MarkerEditActivity extends AbstractActivity {
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
private static final int CAMERA_REQUEST_CODE = 5;
private static final int GALLERY_IMG_REQUEST_CODE = 7;
private static final String TAG = MarkerEditActivity.class.getSimpleName();
private long trackId;
private long markerId;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private Waypoint waypoint;
private MenuItem insertPhotoMenuItem;
private MenuItem insertGalleryImgMenuItem;
private Uri photoUri;
private boolean hasCamera;
// UI elements
private EditText waypointName;
private AutoCompleteTextView waypointMarkerType;
private EditText waypointDescription;
private ImageView waypointPhoto;
private ImageView waypointDeletePhotoBtn;
private Button done;
@Override
@@ -57,7 +88,9 @@ public class MarkerEditActivity extends AbstractActivity {
trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(null);
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
// Setup UI elements
waypointName = findViewById(R.id.marker_edit_waypoint_name);
@@ -65,6 +98,19 @@ public class MarkerEditActivity extends AbstractActivity {
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.waypoint_types, android.R.layout.simple_dropdown_item_1line);
waypointMarkerType.setAdapter(adapter);
waypointDescription = findViewById(R.id.marker_edit_waypoint_description);
waypointPhoto = findViewById(R.id.marker_edit_waypoint_photo);
waypointDeletePhotoBtn = findViewById(R.id.marker_edit_waypoint_photo_delete);
waypointDeletePhotoBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (waypoint != null && waypoint.hasPhoto())
waypoint.setPhotoUrl(null);
waypointPhoto.setImageBitmap(null);
photoUri = null;
hideAndShowOptions();
}
});
Button cancel = findViewById(R.id.marker_edit_cancel);
cancel.setOnClickListener(new OnClickListener() {
@@ -94,6 +140,64 @@ public class MarkerEditActivity extends AbstractActivity {
return R.layout.marker_edit;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.marker_edit, menu);
insertPhotoMenuItem = menu.findItem(R.id.marker_edit_insert_photo);
insertPhotoMenuItem.setVisible(hasCamera);
insertGalleryImgMenuItem = menu.findItem(R.id.marker_edit_insert_gallery_img);
hideAndShowOptions();
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.marker_edit_insert_photo:
createWaypointWithPicture();
return true;
case R.id.marker_edit_insert_gallery_img:
createWaypointWithGalleryImage();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAMERA_REQUEST_CODE) {
if (resultCode == RESULT_CANCELED) {
Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show();
return;
} else if (resultCode == RESULT_OK) {
setWaypointImageView(photoUri);
}
} else if (requestCode == GALLERY_IMG_REQUEST_CODE) {
if (resultCode == RESULT_CANCELED) {
Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show();
return;
} else if (resultCode == RESULT_OK) {
Uri srcUri = data.getData();
try (ParcelFileDescriptor parcelFd = getContentResolver().openFileDescriptor(srcUri, "r")) {
FileDescriptor srcFd = parcelFd.getFileDescriptor();
File dstFile = new File(FileUtils.getImageUrl(this, getTrackId()));
FileUtils.copy(srcFd, dstFile);
photoUri = FileUtils.getUriForFile(this, dstFile);
setWaypointImageView(photoUri);
} catch(Exception e) {
Log.e(TAG, e.getMessage());
Toast.makeText(this, R.string.marker_add_canceled, Toast.LENGTH_LONG).show();
return;
}
}
}
super.onActivityResult(requestCode, resultCode, data);
}
/**
* Updates the UI based on the marker id.
*/
@@ -133,7 +237,62 @@ public class MarkerEditActivity extends AbstractActivity {
waypointName.setText(waypoint.getName());
waypointMarkerType.setText(waypoint.getCategory());
waypointDescription.setText(waypoint.getDescription());
if (waypoint.hasPhoto()) {
photoUri = waypoint.getPhotoURI();
setWaypointImageView(photoUri);
}
}
hideAndShowOptions();
}
/**
* Checks and hide/shows all buttons/options about marker photo options.
*
* If a photo is set then one's options are shown, otherwise another ones are shown.
*/
private void hideAndShowOptions() {
boolean isPhotoSet = (waypoint != null && waypoint.hasPhoto()) || photoUri != null;
if (insertPhotoMenuItem != null && insertGalleryImgMenuItem != null) {
insertPhotoMenuItem.setVisible(!isPhotoSet);
insertGalleryImgMenuItem.setVisible(!isPhotoSet);
}
waypointDeletePhotoBtn.setVisibility(isPhotoSet ? View.VISIBLE : View.GONE);
}
/**
* Returns the trackId. If trackId == -1 then get it from waypoint object.
*/
private long getTrackId() {
return trackId == -1 ? waypoint.getTrackId() : trackId;
}
/**
* Sets the ImageView waypointPhoto with the uri photo.
*
* @param uri the uri photo.
*/
private void setWaypointImageView(Uri uri) {
try (ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r")) {
FileDescriptor fd = pfd.getFileDescriptor();
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd);
waypointPhoto.setImageBitmap(bitmap);
hideAndShowOptions();
} catch(IOException e) {
Log.e(TAG, e.getMessage());
Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show();
}
}
private void createWaypointWithPicture() {
Pair<Intent, Uri> intentAndPhotoUri = IntentUtils.createTakePictureIntent(this, getTrackId());
photoUri = intentAndPhotoUri.second;
startActivityForResult(intentAndPhotoUri.first, CAMERA_REQUEST_CODE);
}
private void createWaypointWithGalleryImage() {
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, GALLERY_IMG_REQUEST_CODE);
}
/**
@@ -144,7 +303,7 @@ public class MarkerEditActivity extends AbstractActivity {
waypointName.getText().toString(),
waypointMarkerType.getText().toString(),
waypointDescription.getText().toString(),
null);
photoUri != null ? photoUri.toString() : null);
}
/**
@@ -154,6 +313,7 @@ public class MarkerEditActivity extends AbstractActivity {
waypoint.setName(waypointName.getText().toString());
waypoint.setCategory(waypointMarkerType.getText().toString());
waypoint.setDescription(waypointDescription.getText().toString());
waypoint.setPhotoUrl(photoUri != null ? photoUri.toString() : null);
new ContentProviderUtils(this).updateWaypoint(waypoint);
}
@@ -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;
@@ -61,7 +61,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
private static final String[] PROJECTION = new String[]{WaypointsColumns._ID,
WaypointsColumns.NAME, WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY,
WaypointsColumns.TYPE, WaypointsColumns.TIME, WaypointsColumns.PHOTOURL,
WaypointsColumns.TIME, WaypointsColumns.PHOTOURL,
WaypointsColumns.LATITUDE, WaypointsColumns.LONGITUDE};
private ContentProviderUtils contentProviderUtils;
@@ -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;
@@ -182,7 +179,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
new String[]{String.valueOf(track.getId())}, null);
} else {
return new CursorLoader(MarkerListActivity.this, WaypointsColumns.CONTENT_URI, PROJECTION,
WaypointsColumns.STARTTIME + " IS NULL", null, null);
null, null, null);
}
}
@@ -40,17 +40,17 @@ 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;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
@@ -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);
@@ -172,7 +172,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(null);
searchEngine = new SearchEngine(contentProviderUtils);
arrayAdapter = new ArrayAdapter<Map<String, Object>>(this, R.layout.list_item, R.id.list_item_name) {
@@ -424,7 +424,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
private Map<String, Object> prepareTrackForDisplay(Track track) {
Map<String, Object> resultMap = new HashMap<>();
TripStatistics tripStatistics = track.getTripStatistics();
TrackStatistics trackStatistics = track.getTrackStatistics();
String icon = track.getIcon();
String category = icon != null && !icon.equals("") ? null : track.getCategory();
@@ -433,10 +433,10 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
resultMap.put(ICON_ID_FIELD, TrackIconUtils.getIconDrawable(icon));
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_track);
resultMap.put(NAME_FIELD, track.getName());
resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(tripStatistics.getTotalTime()));
resultMap.put(TOTAL_DISTANCE_FIELD, StringUtils.formatDistance(this, tripStatistics.getTotalDistance(), metricUnits));
resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(trackStatistics.getTotalTime()));
resultMap.put(TOTAL_DISTANCE_FIELD, StringUtils.formatDistance(this, trackStatistics.getTotalDistance(), metricUnits));
resultMap.put(MARKER_COUNT_FIELD, contentProviderUtils.getWaypointCount(track.getId()));
resultMap.put(START_TIME_FIELD, tripStatistics.getStartTime());
resultMap.put(START_TIME_FIELD, trackStatistics.getStartTime_ms());
resultMap.put(CATEGORY_FIELD, category);
resultMap.put(DESCRIPTION_FIELD, track.getDescription());
resultMap.put(PHOTO_URL_FIELD, null);
@@ -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;
}
}
@@ -16,16 +16,12 @@
package de.dennisguse.opentracks;
import android.Manifest;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Pair;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -34,24 +30,22 @@ 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;
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.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -59,7 +53,7 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils;
/**
* An activity to show the track detail.
* An activity to show the track detail, record a new track or resumes an existing one.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
@@ -72,16 +66,8 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
private static final String TAG = TrackDetailActivity.class.getSimpleName();
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
private static final String PHOTO_URI_KEY = "photo_uri_key";
private static final String HAS_PHOTO_KEY = "has_photo_key";
private static final int CAMERA_REQUEST_CODE = 5;
private static final int EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 6;
// The following are set in onCreate
private boolean hasCamera;
private Uri photoUri;
private boolean hasPhoto;
private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
@@ -104,16 +90,39 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
@Override
public void run() {
trackController.update(isRecording(), recordingTrackPaused);
if (hasPhoto && photoUri != null) {
hasPhoto = false;
long markerId = trackRecordingServiceConnection.addMarker(TrackDetailActivity.this, null, null, null, photoUri.toString());
}
}
});
if (recordingTrackId == -1L) {
TrackRecordingServiceInterface service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.d(TAG, "could not get TrackRecordingService");
return;
}
// Starts or resumes a track.
int msg;
if (trackId == -1L) {
// trackId isn't initialized -> leads a new recording.
trackId = service.startNewTrack();
recordingTrackId = trackId;
msg = R.string.track_detail_record_success;
} else {
// trackId is initialized -> resumes the track.
recordingTrackId = trackId;
service.resumeTrack(trackId);
msg = R.string.track_detail_resume_success;
}
// A recording track is on.
Toast.makeText(TrackDetailActivity.this, msg, Toast.LENGTH_SHORT).show();
trackDataHub.loadTrack(trackId);
trackController.update(true, false);
trackController.onResume(true, 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) {
@@ -151,9 +160,9 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
};
private MenuItem insertMarkerMenuItem;
private MenuItem insertPhotoMenuItem;
private MenuItem markerListMenuItem;
private MenuItem shareMenuItem;
private MenuItem resumeMenuItem;
private final OnClickListener recordListener = new OnClickListener() {
@Override
@@ -185,17 +194,13 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
super.onCreate(savedInstanceState);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
photoUri = savedInstanceState != null ? (Uri) savedInstanceState.getParcelable(PHOTO_URI_KEY) : null;
hasPhoto = savedInstanceState != null && savedInstanceState.getBoolean(HAS_PHOTO_KEY, false);
contentProviderUtils = new ContentProviderUtils(this);
handleIntent(getIntent());
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
trackDataHub = TrackDataHub.newInstance(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
trackDataHub = new TrackDataHub(this);
FragmentPagerAdapter adapter = new FragmentPagerAdapter(getSupportFragmentManager(), 1) {
@Override
@@ -207,17 +212,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) {
@@ -287,11 +290,16 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
@Override
protected void onResume() {
super.onResume();
trackDataHub.loadTrack(trackId);
// Update UI
this.invalidateOptionsMenu();
trackController.onResume(isRecording(), recordingTrackPaused);
if (trackId != -1L) {
trackDataHub.loadTrack(trackId);
trackController.onResume(isRecording(), recordingTrackPaused);
} else {
startRecording();
}
}
@Override
@@ -312,23 +320,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(CURRENT_TAB_TAG_KEY, pager.getCurrentItem());
if (photoUri != null) {
outState.putParcelable(PHOTO_URI_KEY, photoUri);
}
outState.putBoolean(HAS_PHOTO_KEY, hasPhoto);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAMERA_REQUEST_CODE) {
if (resultCode == RESULT_CANCELED) {
Toast.makeText(this, R.string.marker_add_canceled, Toast.LENGTH_LONG).show();
return;
}
hasPhoto = resultCode == RESULT_OK;
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
@@ -348,10 +339,9 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
getMenuInflater().inflate(R.menu.track_detail, menu);
insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker);
insertPhotoMenuItem = menu.findItem(R.id.track_detail_insert_photo);
insertPhotoMenuItem.setVisible(new Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(getPackageManager()) != null);
shareMenuItem = menu.findItem(R.id.track_detail_share);
markerListMenuItem = menu.findItem(R.id.track_detail_markers);
resumeMenuItem = menu.findItem(R.id.track_detail_resume_track);
return super.onCreateOptionsMenu(menu);
}
@@ -377,9 +367,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.track_detail_insert_photo:
createWaypointWithPicture();
return true;
case R.id.track_detail_menu_show_on_map:
IntentUtils.showTrackOnMap(this, new long[]{trackId});
return true;
@@ -396,6 +383,9 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
case R.id.track_detail_delete:
deleteTracks(new long[]{trackId});
return true;
case R.id.track_detail_resume_track:
startRecording();
return true;
case R.id.track_detail_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
@@ -405,20 +395,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
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 {
createWaypointWithPicture();
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return trackRecordingServiceConnection;
@@ -452,9 +428,13 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
return;
}
trackId = waypoint.getTrackId();
if (trackId == -1L) {
finish();
return;
}
}
if (trackId == -1L) {
finish();
return;
}
Track track = contentProviderUtils.getTrack(trackId);
@@ -476,25 +456,21 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
*/
private void updateMenuItems(boolean isPaused) {
insertMarkerMenuItem.setVisible(isRecording() && !isPaused);
insertPhotoMenuItem.setVisible(hasCamera && isRecording() && !isPaused);
shareMenuItem.setVisible(!isRecording());
markerListMenuItem.setShowAsAction(isRecording() ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_IF_ROOM);
String title;
if (isRecording()) {
title = getString(isPaused ? R.string.generic_paused : R.string.generic_recording);
resumeMenuItem.setVisible(false);
} else {
Track track = contentProviderUtils.getTrack(trackId);
title = track != null ? track.getName() : "";
// Only visible if there aren't other tracks that is recording.
resumeMenuItem.setVisible(recordingTrackId == -1L);
}
setTitle(title);
}
private void createWaypointWithPicture() {
Pair<Intent, Uri> intentAndPhotoUri = IntentUtils.createTakePictureIntent(this, trackId);
photoUri = intentAndPhotoUri.second;
startActivityForResult(intentAndPhotoUri.first, CAMERA_REQUEST_CODE);
}
public void chooseActivityType(String category) {
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), category);
}
@@ -509,4 +485,15 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
private boolean isRecording() {
return trackId == recordingTrackId;
}
private void startRecording() {
trackRecordingServiceConnection.startAndBind(this);
/*
* If the binding has happened, then invoke the callback to start a new recording.
* If the binding hasn't happened, then invoking the callback will have no effect.
* But when the binding occurs, the callback will get invoked.
*/
bindChangedCallback.run();
}
}
@@ -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;
@@ -65,7 +65,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(null);
long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
if (trackId == -1L) {
Log.e(TAG, "invalid trackId");
@@ -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
@@ -164,7 +160,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Menu items
private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem;
// private MenuItem deleteAllMenuItem;
private final OnClickListener stopListener = new OnClickListener() {
@Override
@@ -176,8 +171,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
private boolean startGps = false; // true to start gps
private boolean startNewRecording = false; // true to start a new recording
// Callback when the trackRecordingServiceConnection binding changes.
private final Runnable bindChangedCallback = new Runnable() {
@Override
@@ -192,26 +185,15 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
});
if (!startGps && !startNewRecording) {
if (!startGps) {
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;
}
if (startNewRecording) {
startGps = false;
long trackId = service.startNewTrack();
startNewRecording = false;
Intent intent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
Toast.makeText(TrackListActivity.this, R.string.track_list_record_success, Toast.LENGTH_SHORT)
.show();
}
if (startGps) {
service.startGps();
startGps = false;
@@ -224,18 +206,19 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
if (!PreferencesUtils.isRecording(recordingTrackId)) {
// Not recording -> Recording
updateMenuItems(false, true);
startRecording();
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class);
startActivity(newIntent);
} else if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false, true);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(false, true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
// Paused -> Resume
updateMenuItems(false, true);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(false, true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
}
};
@@ -248,7 +231,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
trackController = new TrackController(this, trackRecordingServiceConnection, true, recordListener, stopListener);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
@@ -409,7 +392,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();
}
@@ -460,7 +443,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
/**
* Shows start up dialogs.
*/
public void showStartupDialogs() {
private void showStartupDialogs() {
// If stats_units_key is undefined, set it
if (PreferencesUtils.getString(this, R.string.stats_units_key, "").equals("")) {
String statsUnits = getString(Locale.US.equals(Locale.getDefault()) ? R.string.stats_units_imperial : R.string.stats_units_metric);
@@ -483,7 +466,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
* @param isRecording true if recording
*/
private void updateMenuItems(boolean isGpsStarted, boolean isRecording) {
boolean hasTrack = listView != null && listView.getCount() != 0;
if (startGpsMenuItem != null) {
startGpsMenuItem.setVisible(!isRecording);
if (!isRecording) {
@@ -493,21 +475,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
}
/**
* Starts a new recording.
*/
private void startRecording() {
startNewRecording = true;
trackRecordingServiceConnection.startAndBind(this);
/*
* If the binding has happened, then invoke the callback to start a new recording.
* If the binding hasn't happened, then invoking the callback will have no effect.
* But when the binding occurs, the callback will get invoked.
*/
bindChangedCallback.run();
}
/**
* Handles a context item selection.
*
@@ -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;
@@ -0,0 +1,103 @@
package de.dennisguse.opentracks.chart;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.UnitConversions;
public class ChartPoint {
//X-axis
private double timeOrDistance;
//Y-axis
private double elevation;
private double speed;
private double pace;
private double heartRate = Double.NaN;
private double cadence = Double.NaN;
private double power = Double.NaN;
@VisibleForTesting
ChartPoint(double elevation) {
this.elevation = elevation;
}
public ChartPoint(@NonNull TrackStatisticsUpdater trackStatisticsUpdater, SensorDataSet sensorDataSet, boolean chartByDistance, boolean metricUnits) {
TrackStatistics trackStatistics = trackStatisticsUpdater.getTrackStatistics();
if (chartByDistance) {
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
timeOrDistance = distance;
} else {
timeOrDistance = trackStatistics.getTotalTime();
}
elevation = trackStatisticsUpdater.getSmoothedElevation();
if (!metricUnits) {
elevation *= UnitConversions.M_TO_FT;
}
speed = trackStatisticsUpdater.getSmoothedSpeed() * UnitConversions.MS_TO_KMH;
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
}
pace = speed == 0 ? 0.0 : 60.0 / speed;
if (sensorDataSet != null) {
if (sensorDataSet.hasHeartRate()) {
heartRate = sensorDataSet.getHeartRate();
}
if (sensorDataSet.hasCadence()) {
cadence = sensorDataSet.getCadence();
}
if (sensorDataSet.hasPower()) {
power = sensorDataSet.getPower();
}
}
}
public double getTimeOrDistance() {
return timeOrDistance;
}
public double getElevation() {
return elevation;
}
public double getSpeed() {
return speed;
}
public double getPace() {
return pace;
}
public boolean hasHeartRate() {
return Double.isNaN(heartRate);
}
public double getHeartRate() {
return heartRate;
}
public boolean hasCadence() {
return Double.isNaN(cadence);
}
public double getCadence() {
return cadence;
}
public boolean hasPower() {
return Double.isNaN(power);
}
public double getPower() {
return power;
}
}
@@ -23,6 +23,8 @@ import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Path;
import androidx.annotation.NonNull;
import java.text.NumberFormat;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
@@ -32,7 +34,7 @@ import de.dennisguse.opentracks.stats.ExtremityMonitor;
*
* @author Sandor Dornbush
*/
class ChartValueSeries {
abstract class ChartValueSeries {
private static final float STROKE_WIDTH = 2f;
@@ -126,26 +128,26 @@ class ChartValueSeries {
}
/**
* Updates the series with a new value.
*
* @param value the new value
* Updates the series with a new {@link ChartPoint}.
*/
void update(double value) {
extremityMonitor.update(value);
void update(ChartPoint chartPoint) {
if (isChartPointValid(chartPoint)) {
extremityMonitor.update(extractDataFromChartPoint(chartPoint));
}
}
/**
* Gets the path.
*/
abstract double extractDataFromChartPoint(@NonNull ChartPoint chartPoint);
boolean isChartPointValid(@NonNull ChartPoint chartPoint) {
return !Double.isNaN(extractDataFromChartPoint(chartPoint));
}
protected abstract boolean drawIfChartPointHasNoData();
Path getPath() {
return path;
}
/**
* Draws the path on canvas.
*
* @param canvas the canvas
*/
void drawPath(Canvas canvas) {
canvas.drawPath(path, fillPaint);
canvas.drawPath(path, strokePaint);
@@ -33,6 +33,7 @@ import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.Scroller;
import androidx.annotation.NonNull;
import androidx.core.view.GestureDetectorCompat;
import java.text.NumberFormat;
@@ -62,14 +63,6 @@ public class ChartView extends View {
static final int Y_AXIS_INTERVALS = 5;
public static final int NUM_SERIES = 6;
public static final int ELEVATION_SERIES = 0;
public static final int SPEED_SERIES = 1;
public static final int PACE_SERIES = 2;
public static final int HEART_RATE_SERIES = 3;
public static final int CADENCE_SERIES = 4;
public static final int POWER_SERIES = 5;
private static final int TARGET_X_AXIS_INTERVALS = 4;
private static final int MIN_ZOOM_LEVEL = 1;
@@ -89,8 +82,11 @@ public class ChartView extends View {
X_FRACTION_FORMAT.setMinimumFractionDigits(1);
}
private final ChartValueSeries[] series = new ChartValueSeries[NUM_SERIES];
private final List<double[]> chartData = new ArrayList<>();
private final List<ChartValueSeries> seriesList = new ArrayList<>();
private final ChartValueSeries speedSeries;
private final ChartValueSeries paceSeries;
private final List<ChartPoint> chartPoints = new ArrayList<>();
private final List<Waypoint> waypoints = new ArrayList<>();
private final ExtremityMonitor xExtremityMonitor = new ExtremityMonitor();
private final Paint axisPaint;
@@ -122,7 +118,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 +172,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) {
@@ -192,58 +188,124 @@ public class ChartView extends View {
}
});
public ChartView(Context context, boolean chartByDistance) {
public ChartView(Context context, final boolean chartByDistance) {
super(context);
this.chartByDistance = chartByDistance;
series[ELEVATION_SERIES] = new ChartValueSeries(context,
seriesList.add(new ChartValueSeries(context,
Integer.MIN_VALUE,
Integer.MAX_VALUE,
new int[]{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000},
R.string.description_elevation_metric,
R.string.description_elevation_imperial,
R.color.chart_elevation_fill,
R.color.chart_elevation_border);
series[SPEED_SERIES] = new ChartValueSeries(context,
R.color.chart_elevation_border) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getElevation();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return true;
}
});
speedSeries = new ChartValueSeries(context,
0,
Integer.MAX_VALUE,
new int[]{1, 5, 10, 20, 50, 100},
R.string.description_speed_metric,
R.string.description_speed_imperial,
R.color.chart_speed_fill,
R.color.chart_speed_border);
series[PACE_SERIES] = new ChartValueSeries(context,
R.color.chart_speed_border) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getSpeed();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return reportSpeed;
}
};
seriesList.add(speedSeries);
paceSeries = new ChartValueSeries(context,
0,
Integer.MAX_VALUE,
new int[]{1, 2, 5, 10, 15, 20, 30, 60, 120},
R.string.description_pace_metric,
R.string.description_pace_imperial,
R.color.chart_pace_fill,
R.color.chart_pace_border);
series[HEART_RATE_SERIES] = new ChartValueSeries(context,
R.color.chart_pace_border) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getPace();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return !reportSpeed;
}
};
seriesList.add(paceSeries);
seriesList.add(new ChartValueSeries(context,
0,
Integer.MAX_VALUE,
new int[]{25, 50},
R.string.description_sensor_heart_rate,
R.string.description_sensor_heart_rate,
R.color.chart_heart_rate_fill,
R.color.chart_heart_rate_border);
series[CADENCE_SERIES] = new ChartValueSeries(context,
R.color.chart_heart_rate_border) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getHeartRate();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return false;
}
});
seriesList.add(new ChartValueSeries(context,
0,
Integer.MAX_VALUE,
new int[]{5, 10, 25, 50},
R.string.description_sensor_cadence,
R.string.description_sensor_cadence,
R.color.chart_cadence_fill,
R.color.chart_cadence_border);
series[POWER_SERIES] = new ChartValueSeries(context,
R.color.chart_cadence_border) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getCadence();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return false;
}
});
seriesList.add(new ChartValueSeries(context,
0,
1000,
new int[]{5, 50, 100, 200},
R.string.description_sensor_power,
R.string.description_sensor_power,
R.color.chart_power_fill,
R.color.chart_power_border);
R.color.chart_power_border) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getPower();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return false;
}
});
float scale = context.getResources().getDisplayMetrics().density;
@@ -279,6 +341,10 @@ public class ChartView extends View {
setFocusable(true);
setClickable(true);
updateDimensions();
// either speedSeries or paceSeries should be enabled.
speedSeries.setEnabled(reportSpeed);
paceSeries.setEnabled(!reportSpeed);
}
@Override
@@ -286,13 +352,8 @@ public class ChartView extends View {
return true;
}
/**
* Sets the enabled value for a chart value series.
*
* @param index the chart value series index
*/
public void setChartValueSeriesEnabled(int index, boolean enabled) {
series[index].setEnabled(enabled);
public boolean getMetricUnits() {
return metricUnits;
}
/**
@@ -304,38 +365,48 @@ public class ChartView extends View {
metricUnits = value;
}
public boolean getReportSpeed() {
return reportSpeed;
}
/**
* Sets report speed.
*
* @param value true to report speed
* @param value report speed (true) or pace (false)
*/
public void setReportSpeed(boolean value) {
reportSpeed = value;
}
/**
* Sets show pointer.
*
* @param value true to show pointer
*/
public boolean applyReportSpeed() {
if (reportSpeed) {
if (!speedSeries.isEnabled()) {
speedSeries.setEnabled(true);
paceSeries.setEnabled(false);
return true;
}
} else {
if (!paceSeries.isEnabled()) {
speedSeries.setEnabled(false);
paceSeries.setEnabled(true);
return true;
}
}
return false;
}
public void setShowPointer(boolean value) {
showPointer = value;
}
/**
* Adds data points.
*
* @param dataPoints an array of data points to be added
*/
public void addDataPoints(List<double[]> dataPoints) {
synchronized (chartData) {
chartData.addAll(dataPoints);
for (double[] dataPoint : dataPoints) {
xExtremityMonitor.update(dataPoint[0]);
for (int j = 0; j < series.length; j++) {
if (!Double.isNaN(dataPoint[j + 1])) {
series[j].update(dataPoint[j + 1]);
}
public void addChartPoints(List<ChartPoint> dataPoints) {
synchronized (chartPoints) {
chartPoints.addAll(dataPoints);
for (ChartPoint dataPoint : dataPoints) {
xExtremityMonitor.update(dataPoint.getTimeOrDistance());
for (ChartValueSeries i : seriesList) {
i.update(dataPoint);
}
}
updateDimensions();
@@ -347,8 +418,8 @@ public class ChartView extends View {
* Clears all data.
*/
public void reset() {
synchronized (chartData) {
chartData.clear();
synchronized (chartPoints) {
chartPoints.clear();
xExtremityMonitor.reset();
zoomLevel = 1;
updateDimensions();
@@ -356,50 +427,34 @@ public class ChartView extends View {
}
/**
* Resets scroll. To be called on the UI thread.
* Resets scroll.
* To be called on the UI thread.
*/
public void resetScroll() {
scrollTo(0, 0);
}
/**
* Adds a waypoint.
*
* @param waypoint the waypoint
*/
public void addWaypoint(Waypoint waypoint) {
synchronized (waypoints) {
waypoints.add(waypoint);
}
}
/**
* Clears the waypoints.
*/
public void clearWaypoints() {
synchronized (waypoints) {
waypoints.clear();
}
}
/**
* Returns true if can zoom in.
*/
public boolean canZoomIn() {
private boolean canZoomIn() {
return zoomLevel < MAX_ZOOM_LEVEL;
}
/**
* Returns true if can zoom out.
*/
public boolean canZoomOut() {
private boolean canZoomOut() {
return zoomLevel > MIN_ZOOM_LEVEL;
}
/**
* Zooms in one level.
*/
public void zoomIn() {
private void zoomIn() {
if (canZoomIn()) {
zoomLevel++;
updatePaths();
@@ -407,10 +462,7 @@ public class ChartView extends View {
}
}
/**
* Zooms out one level.
*/
public void zoomOut() {
private void zoomOut() {
if (canZoomOut()) {
zoomLevel--;
scroller.abortAnimation();
@@ -430,7 +482,7 @@ public class ChartView extends View {
*
* @param velocityX velocity of fling in pixels per second
*/
public void fling(int velocityX) {
private void fling(int velocityX) {
int maxWidth = effectiveWidth * (zoomLevel - 1);
scroller.fling(getScrollX(), 0, velocityX, 0, 0, maxWidth, 0, 0);
invalidate();
@@ -441,7 +493,7 @@ public class ChartView extends View {
*
* @param deltaX the number of pixels to scroll
*/
public void scrollBy(int deltaX) {
private void scrollBy(int deltaX) {
int scrollX = getScrollX() + deltaX;
if (scrollX < 0) {
scrollX = 0;
@@ -485,8 +537,7 @@ public class ChartView extends View {
@Override
protected void onDraw(Canvas canvas) {
synchronized (chartData) {
synchronized (chartPoints) {
canvas.save();
canvas.drawColor(Color.WHITE);
@@ -529,7 +580,7 @@ public class ChartView extends View {
* @param canvas the canvas
*/
private void drawDataSeries(Canvas canvas) {
for (ChartValueSeries chartValueSeries : series) {
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData()) {
chartValueSeries.drawPath(canvas);
}
@@ -543,11 +594,7 @@ public class ChartView extends View {
*/
private void drawWaypoints(Canvas canvas) {
synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) {
final Waypoint waypoint = waypoints.get(i);
if (waypoint.getLocation() == null) {
continue;
}
for (Waypoint waypoint : waypoints) {
double xValue = getWaypointXValue(waypoint);
if (xValue > maxX) {
continue;
@@ -595,9 +642,8 @@ public class ChartView extends View {
int lines = titleDimensions[0];
int lineHeight = titleDimensions[1];
int count = 0;
for (int i = 0; i < series.length; i++) {
ChartValueSeries chartValueSeries = series[i];
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(i)) {
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
count++;
String title = getContext().getString(chartValueSeries.getTitleId(metricUnits));
Paint paint = chartValueSeries.getTitlePaint();
@@ -615,9 +661,8 @@ public class ChartView extends View {
private int[] getTitleDimensions() {
int lines = 0;
int lineHeight = 0;
for (int i = 0; i < series.length; i++) {
ChartValueSeries chartValueSeries = series[i];
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(i)) {
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
lines++;
String title = getContext().getString(chartValueSeries.getTitleId(metricUnits));
Rect rect = getRect(chartValueSeries.getTitlePaint(), title);
@@ -651,9 +696,6 @@ public class ChartView extends View {
}
}
/**
* Gets the x axis label.
*/
private String getXAxisLabel() {
Context context = getContext();
if (chartByDistance) {
@@ -677,9 +719,6 @@ public class ChartView extends View {
canvas.drawText(marker, getX(value), topBorder + effectiveHeight + spacing + rect.height(), xAxisMarkerPaint);
}
/**
* Gets the x axis interval.
*/
private double getXAxisInterval() {
double interval = maxX / zoomLevel / TARGET_X_AXIS_INTERVALS;
if (interval < 1) {
@@ -694,16 +733,13 @@ public class ChartView extends View {
return interval;
}
/**
* Gets the x axis marker positions.
*/
private List<Double> getXAxisMarkerPositions(double interval) {
List<Double> markers = new ArrayList<>();
markers.add(0d);
for (int i = 1; i * interval < maxX; i++) {
markers.add(i * interval);
}
// At least 2 markers
if (markers.size() < 2) {
markers.add(maxX);
}
@@ -720,11 +756,12 @@ public class ChartView extends View {
int y = topBorder;
canvas.drawLine(x, y, x, y + effectiveHeight, axisPaint);
//TODO
int markerXPosition = x - spacer;
for (int i = 0; i < series.length; i++) {
int index = series.length - 1 - i;
ChartValueSeries chartValueSeries = series[index];
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(index)) {
for (int i = 0; i < seriesList.size(); i++) {
int index = seriesList.size() - 1 - i;
ChartValueSeries chartValueSeries = seriesList.get(index);
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
markerXPosition -= drawYAxisMarkers(chartValueSeries, canvas, markerXPosition) + spacer;
}
}
@@ -772,29 +809,29 @@ public class ChartView extends View {
* @param canvas the canvas
*/
private void drawPointer(Canvas canvas) {
int index = -1;
for (int i = 0; i < series.length; i++) {
ChartValueSeries chartValueSeries = series[i];
ChartValueSeries firstChartValueSeries = null;
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData()) {
index = i;
firstChartValueSeries = chartValueSeries;
break;
}
}
if (index != -1 && chartData.size() > 0) {
if (firstChartValueSeries != null && chartPoints.size() > 0) {
int dx = getX(maxX) - pointer.getIntrinsicWidth() / 2;
int dy = getY(series[index], chartData.get(chartData.size() - 1)[index + 1])
- pointer.getIntrinsicHeight();
double value = firstChartValueSeries.extractDataFromChartPoint(chartPoints.get(chartPoints.size() - 1));
int dy = getY(firstChartValueSeries, value) - pointer.getIntrinsicHeight();
canvas.translate(dx, dy);
pointer.draw(canvas);
}
}
/**
* Updates paths. The path needs to be updated any time after the data or the dimensions change.
* Updates paths.
* The path needs to be updated any time after the data or the dimensions change.
*/
private void updatePaths() {
synchronized (chartData) {
for (ChartValueSeries chartValueSeries : series) {
synchronized (chartPoints) {
for (ChartValueSeries chartValueSeries : seriesList) {
chartValueSeries.getPath().reset();
}
drawPaths();
@@ -802,24 +839,23 @@ public class ChartView extends View {
}
}
/**
* Draws all paths.
*/
private void drawPaths() {
boolean[] hasMoved = new boolean[series.length];
boolean[] hasMoved = new boolean[seriesList.size()];
for (double[] dataPoint : chartData) {
for (int j = 0; j < series.length; j++) {
double value = dataPoint[j + 1];
if (Double.isNaN(value)) {
for (ChartPoint dataPoint : chartPoints) {
for (int i = 0; i < seriesList.size(); i++) {
ChartValueSeries chartValueSeries = seriesList.get(i);
if (chartValueSeries.isChartPointValid(dataPoint)) {
continue;
}
ChartValueSeries chartValueSeries = series[j];
double value = chartValueSeries.extractDataFromChartPoint(dataPoint);
Path path = chartValueSeries.getPath();
int x = getX(dataPoint[0]);
int x = getX(dataPoint.getTimeOrDistance());
int y = getY(chartValueSeries, value);
if (!hasMoved[j]) {
hasMoved[j] = true;
if (!hasMoved[i]) {
hasMoved[i] = true;
path.moveTo(x, y);
} else {
path.lineTo(x, y);
@@ -828,37 +864,32 @@ public class ChartView extends View {
}
}
/**
* Closes all paths.
*/
private void closePaths() {
for (int i = 0; i < series.length; i++) {
int first = getFirstPopulatedChartDataIndex(i);
for (ChartValueSeries chartValueSeries : seriesList) {
int first = getFirstPopulatedChartDataIndex(chartValueSeries);
if (first != -1) {
int xCorner = getX(chartData.get(first)[0]);
int xCorner = getX(chartPoints.get(first).getTimeOrDistance());
int yCorner = topBorder + effectiveHeight;
ChartValueSeries chartValueSeries = series[i];
Path path = chartValueSeries.getPath();
// Bottom right corner
path.lineTo(getX(chartData.get(chartData.size() - 1)[0]), yCorner);
path.lineTo(getX(chartPoints.get(chartPoints.size() - 1).getTimeOrDistance()), yCorner);
// Bottom left corner
path.lineTo(xCorner, yCorner);
// Top right corner
path.lineTo(xCorner, getY(chartValueSeries, chartData.get(first)[i + 1]));
double value = chartValueSeries.extractDataFromChartPoint(chartPoints.get(first));
path.lineTo(xCorner, getY(chartValueSeries, value));
}
}
}
/**
* Finds the index of the first data point containing data for a series.
* Returns -1 if no data point contains data for the series.
*
* @param seriesIndex the series's index
* @return -1 if no data point contains data for the series.
*/
private int getFirstPopulatedChartDataIndex(int seriesIndex) {
for (int i = 0; i < chartData.size(); i++) {
if (!Double.isNaN(chartData.get(i)[seriesIndex + 1])) {
private int getFirstPopulatedChartDataIndex(ChartValueSeries chartValueSeries) {
for (int i = 0; i < chartPoints.size(); i++) {
if (chartValueSeries.isChartPointValid(chartPoints.get(i))) {
return i;
}
}
@@ -870,7 +901,7 @@ public class ChartView extends View {
*/
private void updateDimensions() {
maxX = xExtremityMonitor.hasData() ? xExtremityMonitor.getMax() : 1.0;
for (ChartValueSeries chartValueSeries : series) {
for (ChartValueSeries chartValueSeries : seriesList) {
chartValueSeries.updateDimension();
}
float density = getContext().getResources().getDisplayMetrics().density;
@@ -878,9 +909,8 @@ public class ChartView extends View {
yAxisOffset = (int) (density * Y_AXIS_OFFSET);
int markerLength = 0;
for (int i = 0; i < series.length; i++) {
ChartValueSeries chartValueSeries = series[i];
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(i)) {
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
Rect rect = getRect(chartValueSeries.getMarkerPaint(), chartValueSeries.getLargestMarker());
markerLength += rect.width() + spacer;
}
@@ -973,22 +1003,12 @@ public class ChartView extends View {
/**
* Returns true if the index is allowed when the chartData is empty.
*
* @param index the index
*/
private boolean allowIfEmpty(int index) {
if (!chartData.isEmpty()) {
private boolean allowIfEmpty(ChartValueSeries chartValueSeries) {
if (!chartPoints.isEmpty()) {
return false;
}
switch (index) {
case ELEVATION_SERIES:
return true;
case SPEED_SERIES:
return reportSpeed;
case PACE_SERIES:
return !reportSpeed;
default:
return false;
}
return chartValueSeries.drawIfChartPointHasNoData();
}
}
@@ -1,79 +0,0 @@
/*
* Copyright 2011 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.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.
*
* @author Rodrigo Damazio
*/
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);
}
}
@@ -1,48 +0,0 @@
/*
* Copyright 2011 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.content;
/**
* Listener to be invoked when {@link DataSource} changes.
*
* @author Jimmy Shih
*/
public interface DataSourceListener {
/**
* Notifies when the tracks table is updated.
*/
void notifyTracksTableUpdated();
/**
* Notifies when the waypoints table is updated.
*/
void notifyWaypointsTableUpdated();
/**
* Notifies when the track points table is updated.
*/
void notifyTrackPointsTableUpdated();
/**
* Notifies when a preference changes.
*
* @param key the preference key
*/
void notifyPreferenceChanged(String key);
}
@@ -1,234 +0,0 @@
/*
* Copyright 2011 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.content;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.ContentObserver;
import android.os.Handler;
import android.util.Log;
import java.util.EnumSet;
import java.util.Set;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
/**
* Creates observers/listeners and manages their registration with {@link DataSource}.
* The observers/listeners calls {@link DataSourceListener} when data changes.
*
* @author Rodrigo Damazio
*/
class DataSourceManager {
private static final String TAG = DataSourceManager.class.getSimpleName();
private final DataSource dataSource;
private final DataSourceListener dataSourceListener;
// Registered listeners
private final Set<TrackDataType> registeredListeners = EnumSet.noneOf(TrackDataType.class);
private final Handler handler;
private final TracksTableObserver tracksTableObserver;
private final WaypointsTableObserver waypointsTableObserver;
private final TrackPointsTableObserver trackPointsTableObserver;
private final PreferenceListener preferenceListener;
DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) {
this.dataSource = dataSource;
this.dataSourceListener = dataSourceListener;
handler = new Handler();
tracksTableObserver = new TracksTableObserver();
waypointsTableObserver = new WaypointsTableObserver();
trackPointsTableObserver = new TrackPointsTableObserver();
preferenceListener = new PreferenceListener();
}
/**
* Updates listeners with data source.
*
* @param listeners the listeners
*/
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
*/
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);
}
Log.d(TAG, "Updating listeners " + neededListeners);
// Unnecessary = registered - needed
Set<TrackDataType> unnecessaryListeners = EnumSet.copyOf(registeredListeners);
unnecessaryListeners.removeAll(neededListeners);
// Missing = needed - registered
Set<TrackDataType> missingListeners = EnumSet.copyOf(neededListeners);
missingListeners.removeAll(registeredListeners);
// Remove unnecessary listeners
for (TrackDataType trackDataType : unnecessaryListeners) {
unregisterListener(trackDataType);
}
// Add missing listeners
for (TrackDataType trackDataType : missingListeners) {
registerListener(trackDataType);
}
// Update registered listeners
registeredListeners.clear();
registeredListeners.addAll(neededListeners);
}
/**
* Registers a listener with data source.
*
* @param trackDataType the listener data type
*/
private void registerListener(TrackDataType trackDataType) {
switch (trackDataType) {
case TRACKS_TABLE:
dataSource.registerContentObserver(TracksColumns.CONTENT_URI, tracksTableObserver);
break;
case WAYPOINTS_TABLE:
dataSource.registerContentObserver(WaypointsColumns.CONTENT_URI, waypointsTableObserver);
break;
case SAMPLED_IN_TRACK_POINTS_TABLE:
dataSource.registerContentObserver(
TrackPointsColumns.CONTENT_URI, 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;
}
}
/**
* Unregisters a listener with data source.
*
* @param trackDataType listener data type
*/
private void unregisterListener(TrackDataType trackDataType) {
switch (trackDataType) {
case TRACKS_TABLE:
dataSource.unregisterContentObserver(tracksTableObserver);
break;
case WAYPOINTS_TABLE:
dataSource.unregisterContentObserver(waypointsTableObserver);
break;
case SAMPLED_IN_TRACK_POINTS_TABLE:
dataSource.unregisterContentObserver(trackPointsTableObserver);
break;
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;
}
}
/**
* Unregisters all listeners with data source.
*/
void unregisterAllListeners() {
for (TrackDataType trackDataType : TrackDataType.values()) {
unregisterListener(trackDataType);
}
}
/**
* Observer when the tracks table is updated.
*
* @author Jimmy Shih
*/
private class TracksTableObserver extends ContentObserver {
TracksTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
dataSourceListener.notifyTracksTableUpdated();
}
}
/**
* Observer when the waypoints table is updated.
*
* @author Jimmy Shih
*/
private class WaypointsTableObserver extends ContentObserver {
WaypointsTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
dataSourceListener.notifyWaypointsTableUpdated();
}
}
/**
* Observer when the track points table is updated.
*
* @author Jimmy Shih
*/
private class TrackPointsTableObserver extends ContentObserver {
TrackPointsTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
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);
}
}
}
@@ -23,7 +23,7 @@ import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -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;
@@ -67,7 +67,7 @@ public class DescriptionGenerator {
writeString(track.getName(), builder, R.string.generic_name_line, lineBreak);
writeString(track.getCategory(), builder, R.string.description_activity_type, lineBreak);
writeString(track.getDescription(), builder, R.string.generic_description_line, lineBreak);
builder.append(generateTripStatisticsDescription(track.getTripStatistics(), html));
builder.append(generateTrackStatisticsDescription(track.getTrackStatistics(), html));
return builder.toString();
}
@@ -89,12 +89,12 @@ public class DescriptionGenerator {
}
/**
* Generates a description for a {@link TripStatistics}.
* Generates a description for a {@link TrackStatistics}.
*
* @param stats the trip statistics
* @param stats the track statistics
* @param html true to use "<br>" for line break instead of "\n"
*/
private String generateTripStatisticsDescription(TripStatistics stats, boolean html) {
private String generateTrackStatisticsDescription(TrackStatistics stats, boolean html) {
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
@@ -134,14 +134,8 @@ public class DescriptionGenerator {
// Elevation gain
writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
// Max grade
writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak);
// Min grade
writeGrade(stats.getMinGrade(), builder, R.string.description_min_grade, lineBreak);
// Recorded time
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime())));
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime_ms())));
builder.append(lineBreak);
return builder.toString();
@@ -228,19 +222,4 @@ public class DescriptionGenerator {
builder.append(context.getString(resId, elevationInM, elevationInFt));
builder.append(lineBreak);
}
/**
* Writes grade.
*
* @param grade grade in fraction
* @param builder StringBuilder to append grade
* @param resId resource id grade string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeGrade(double grade, StringBuilder builder, int resId, String lineBreak) {
long gradeInPercent = Double.isNaN(grade) || Double.isInfinite(grade) ? 0L : Math.round(grade * 100);
builder.append(context.getString(resId, gradeInPercent));
builder.append(lineBreak);
}
}
@@ -1,22 +0,0 @@
package de.dennisguse.opentracks.content;
import android.location.Location;
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 LocationFactory {
/**
* The default {@link LocationFactory} which creates a location each time.
*/
public static LocationFactory DEFAULT_LOCATION_FACTORY = new LocationFactory();
public Location createLocation() {
return new TrackPoint(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,7 +32,8 @@ 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.stats.TripStatistics;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
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
@@ -246,9 +247,9 @@ public class SearchEngine {
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
TripStatistics statistics = track.getTripStatistics();
TrackStatistics statistics = track.getTrackStatistics();
long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L;
long meanTimestamp = (statistics.getStartTime_ms() + statistics.getStopTime_ms()) / 2L;
score *= getTimeBoost(query, meanTimestamp);
// Score the currently-selected track lower (user is already there, wouldn't be searching for it).
@@ -16,9 +16,11 @@
package de.dennisguse.opentracks.content;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.database.Cursor;
import android.location.Location;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
@@ -26,25 +28,29 @@ import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.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.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* Track data hub. Receives data from {@link DataSource} and distributes it to {@link TrackDataListener} after some processing.
* Track data hub.
* Receives data from {@link de.dennisguse.opentracks.content.provider.CustomContentProvider} and distributes it to {@link TrackDataListener} after some processing.
*
* {@link TrackPoint}s are filtered/downsampled with a dynamic sampling frequency.
*
* @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 SharedPreferences.OnSharedPreferenceChangeListener {
/**
* Target number of track points displayed by the map overlay.
@@ -70,31 +76,26 @@ public class TrackDataHub implements DataSourceListener {
private boolean started;
private HandlerThread handlerThread;
private Handler handler;
private DataSource dataSource;
private DataSourceManager dataSourceManager;
// Preference values
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;
private long firstSeenTrackPointId;
private long lastSeenTrackPointId;
// Registered listeners
private ContentObserver tracksTableObserver;
private ContentObserver waypointsTableObserver;
private ContentObserver trackPointsTableObserver;
public TrackDataHub(Context context) {
this(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACK_POINTS);
}
/**
* 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
*/
@VisibleForTesting
private TrackDataHub(Context context, TrackDataManager trackDataManager, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
this.context = context;
@@ -110,18 +111,43 @@ public class TrackDataHub implements DataSourceListener {
return;
}
started = true;
handlerThread = new HandlerThread("TrackDataHubHandlerThread");
handlerThread = new HandlerThread(TAG);
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
dataSource = newDataSource();
dataSourceManager = new DataSourceManager(dataSource, this);
notifyPreferenceChanged(null);
//register listeners
ContentResolver contentResolver = context.getContentResolver();
tracksTableObserver = new ContentObserver(handler) {
@Override
public void onChange(boolean selfChange) {
notifyTracksTableUpdate(trackDataManager.getListenerTracks());
}
};
contentResolver.registerContentObserver(TracksColumns.CONTENT_URI, false, tracksTableObserver);
waypointsTableObserver = new ContentObserver(handler) {
@Override
public void onChange(boolean selfChange) {
notifyWaypointsTableUpdate(trackDataManager.getListenerWaypoints());
}
};
contentResolver.registerContentObserver(WaypointsColumns.CONTENT_URI, false, waypointsTableObserver);
trackPointsTableObserver = new ContentObserver(handler) {
@Override
public void onChange(boolean selfChange) {
notifyTrackPointsTableUpdate(true, trackDataManager.getListenerTrackPoints_SampledIn(), trackDataManager.getListenerTrackPoints_SampledOut());
}
};
contentResolver.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, false, trackPointsTableObserver);
PreferencesUtils.register(context, this);
onSharedPreferenceChanged(null, null);
runInHandlerThread(new Runnable() {
@Override
public void run() {
if (dataSourceManager != null) {
dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes());
if (started) {
loadDataForAll();
}
}
@@ -133,16 +159,22 @@ public class TrackDataHub implements DataSourceListener {
Log.i(TAG, "TrackDataHub not started, ignoring stop.");
return;
}
PreferencesUtils.unregister(context, this);
started = false;
dataSourceManager.unregisterAllListeners();
//Unregister listeners
ContentResolver contentResolver = context.getContentResolver();
contentResolver.unregisterContentObserver(tracksTableObserver);
contentResolver.unregisterContentObserver(waypointsTableObserver);
contentResolver.unregisterContentObserver(trackPointsTableObserver);
if (handlerThread != null) {
handlerThread.getLooper().quit();
handlerThread = null;
}
handler = null;
dataSource = null;
dataSourceManager = null;
}
public void loadTrack(final long trackId) {
@@ -163,15 +195,13 @@ public class TrackDataHub implements DataSourceListener {
* Registers a {@link TrackDataListener}.
*
* @param trackDataListener the track data listener
* @param trackDataTypes the track data types
*/
public void registerTrackDataListener(final TrackDataListener trackDataListener, final EnumSet<TrackDataType> trackDataTypes) {
public void registerTrackDataListener(final TrackDataListener trackDataListener, final boolean tracksTable, final boolean waypointsTable, final boolean trackPointsTable_SampleIn, final boolean trackPointsTable_SampleOut) {
runInHandlerThread(new Runnable() {
@Override
public void run() {
trackDataManager.registerListener(trackDataListener, trackDataTypes);
if (dataSourceManager != null) {
dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes());
trackDataManager.registerTrackDataListener(trackDataListener, tracksTable, waypointsTable, trackPointsTable_SampleIn, trackPointsTable_SampleOut);
if (started) {
loadDataForListener(trackDataListener);
}
}
@@ -187,22 +217,7 @@ public class TrackDataHub implements DataSourceListener {
runInHandlerThread(new Runnable() {
@Override
public void run() {
trackDataManager.unregisterListener(trackDataListener);
if (dataSourceManager != null) {
dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes());
}
}
});
}
/**
* Reloads data for a {@link TrackDataListener}.
*/
public void reloadDataForListener(final TrackDataListener trackDataListener) {
runInHandlerThread(new Runnable() {
@Override
public void run() {
loadDataForListener(trackDataListener);
trackDataManager.unregisterTrackDataListener(trackDataListener);
}
});
}
@@ -222,38 +237,7 @@ public class TrackDataHub implements DataSourceListener {
}
@Override
public void notifyTracksTableUpdated() {
runInHandlerThread(new Runnable() {
@Override
public void run() {
notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE));
}
});
}
@Override
public void notifyWaypointsTableUpdated() {
runInHandlerThread(new Runnable() {
@Override
public void run() {
notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE));
}
});
}
@Override
public void notifyTrackPointsTableUpdated() {
runInHandlerThread(new Runnable() {
@Override
public void run() {
notifyTrackPointsTableUpdate(true, trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE),
trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
}
});
}
@Override
public void notifyPreferenceChanged(final String key) {
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key) {
runInHandlerThread(new Runnable() {
@Override
public void run() {
@@ -263,46 +247,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);
}
}
}
}
}
});
}
@@ -312,26 +256,17 @@ public class TrackDataHub implements DataSourceListener {
*/
private void loadDataForAll() {
resetSamplingState();
if (trackDataManager.getNumberOfListeners() == 0) {
if (!trackDataManager.hasListeners()) {
return;
}
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
trackDataListener.onMetricUnitsChanged(metricUnits);
trackDataListener.onReportSpeedChanged(reportSpeed);
trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy);
trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval);
}
notifyTracksTableUpdate(trackDataManager.getListenerTracks());
notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE));
for (TrackDataListener listener : trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)) {
for (TrackDataListener listener : trackDataManager.getListenerTrackPoints_SampledIn()) {
listener.clearTrackPoints();
}
notifyTrackPointsTableUpdate(true,
trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE),
trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE));
notifyTrackPointsTableUpdate(true, trackDataManager.getListenerTrackPoints_SampledIn(), trackDataManager.getListenerTrackPoints_SampledOut());
notifyWaypointsTableUpdate(trackDataManager.getListenerWaypoints());
}
/**
@@ -341,21 +276,13 @@ public class TrackDataHub implements DataSourceListener {
*/
private void loadDataForListener(TrackDataListener trackDataListener) {
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)) {
if (trackDataManager.listensForTracks(trackDataListener)) {
notifyTracksTableUpdate(trackDataListeners);
}
boolean hasSampledIn = trackDataTypes.contains(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE);
boolean hasSampledOut = trackDataTypes.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE);
boolean hasSampledIn = trackDataManager.listensForTrackPoints_SampledIn(trackDataListener);
boolean hasSampledOut = trackDataManager.listensForTrackPoints_SampledOut(trackDataListener);
if (hasSampledIn || hasSampledOut) {
trackDataListener.clearTrackPoints();
boolean isOnlyListener = trackDataManager.getNumberOfListeners() == 1;
@@ -366,7 +293,7 @@ public class TrackDataHub implements DataSourceListener {
notifyTrackPointsTableUpdate(isOnlyListener, trackDataListeners, sampledOutListeners);
}
if (trackDataTypes.contains(TrackDataType.WAYPOINTS_TABLE)) {
if (trackDataManager.listensForWaypoints(trackDataListener)) {
notifyWaypointsTableUpdate(trackDataListeners);
}
}
@@ -441,60 +368,60 @@ public class TrackDataHub implements DataSourceListener {
}
}
int localNumLoadedPoints = updateSamplingState ? numLoadedPoints : 0;
long localFirstSeenLocationId = updateSamplingState ? firstSeenLocationId : -1L;
long localLastSeenLocationId = updateSamplingState ? lastSeenLocationId : -1L;
long maxPointId = updateSamplingState ? -1L : lastSeenLocationId;
int localNumLoadedTrackPoints = updateSamplingState ? numLoadedPoints : 0;
long localFirstSeenTrackPointId = updateSamplingState ? firstSeenTrackPointId : -1L;
long localLastSeenTrackPointIdId = updateSamplingState ? lastSeenTrackPointId : -1L;
long maxPointId = updateSamplingState ? -1L : lastSeenTrackPointId;
long lastTrackPointId = contentProviderUtils.getLastTrackPointId(selectedTrackId);
int samplingFrequency = -1;
boolean includeNextPoint = false;
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenTrackPointIdId + 1, false)) {
while (locationIterator.hasNext()) {
Location location = locationIterator.next();
long locationId = locationIterator.getLocationId();
while (trackPointIterator.hasNext()) {
TrackPoint trackPoint = trackPointIterator.next();
long trackPointId = trackPointIterator.getTrackPointId();
// Stop if past the last wanted point
if (maxPointId != -1L && locationId > maxPointId) {
if (maxPointId != -1L && trackPointId > maxPointId) {
break;
}
if (localFirstSeenLocationId == -1) {
localFirstSeenLocationId = locationId;
if (localFirstSeenTrackPointId == -1) {
localFirstSeenTrackPointId = trackPointId;
}
if (samplingFrequency == -1) {
long numTotalPoints = Math.max(0L, lastTrackPointId - localFirstSeenLocationId);
long numTotalPoints = Math.max(0L, lastTrackPointId - localFirstSeenTrackPointId);
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
}
if (!LocationUtils.isValidLocation(location)) { //This can be split markers (not anymore supported feature)
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) { //This can be split markers (not anymore supported feature)
includeNextPoint = true;
} else {
// Also include the last point if the selected track is not recording.
if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0) || (locationId == lastTrackPointId && !isSelectedTrackRecording())) {
if (includeNextPoint || (localNumLoadedTrackPoints % samplingFrequency == 0) || (trackPointId == lastTrackPointId && !isSelectedTrackRecording())) {
includeNextPoint = false;
for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSampledInTrackPoint(location);
trackDataListener.onSampledInTrackPoint(trackPoint);
}
} else {
for (TrackDataListener trackDataListener : sampledOutListeners) {
trackDataListener.onSampledOutTrackPoint(location);
trackDataListener.onSampledOutTrackPoint(trackPoint);
}
}
}
localNumLoadedPoints++;
localLastSeenLocationId = locationId;
localNumLoadedTrackPoints++;
localLastSeenTrackPointIdId = trackPointId;
}
}
if (updateSamplingState) {
numLoadedPoints = localNumLoadedPoints;
firstSeenLocationId = localFirstSeenLocationId;
lastSeenLocationId = localLastSeenLocationId;
numLoadedPoints = localNumLoadedTrackPoints;
firstSeenTrackPointId = localFirstSeenTrackPointId;
lastSeenTrackPointId = localLastSeenTrackPointIdId;
}
for (TrackDataListener listener : sampledInListeners) {
@@ -507,16 +434,8 @@ public class TrackDataHub implements DataSourceListener {
*/
private void resetSamplingState() {
numLoadedPoints = 0;
firstSeenLocationId = -1L;
lastSeenLocationId = -1L;
}
/**
* Creates a {@link DataSource}.
*/
@VisibleForTesting
private DataSource newDataSource() {
return new DataSource(context);
firstSeenTrackPointId = -1L;
lastSeenTrackPointId = -1L;
}
/**
@@ -524,6 +443,7 @@ public class TrackDataHub implements DataSourceListener {
*
* @param runnable the runnable
*/
@Deprecated //TODO: Why actually catch this problem: I guess it would be better to fail hard.
@VisibleForTesting
private void runInHandlerThread(Runnable runnable) {
if (handler == null) {
@@ -533,41 +453,4 @@ public class TrackDataHub implements DataSourceListener {
}
handler.post(runnable);
}
/**
* Gets the value selectedTrackId.
*
* @return the selectedTrackId
*/
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;
}
}
@@ -16,9 +16,8 @@
package de.dennisguse.opentracks.content;
import android.location.Location;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
/**
@@ -43,20 +42,20 @@ public interface TrackDataListener {
/**
* Called when a sampled in track point is read.
*
* @param location the location
* @param trackPoint the trackPoint
*/
void onSampledInTrackPoint(Location location);
void onSampledInTrackPoint(TrackPoint trackPoint);
/**
* Called when a sampled out track point is read.
*
* @param location the location
* @param trackPoint the trackPoint
*/
void onSampledOutTrackPoint(Location location);
void onSampledOutTrackPoint(TrackPoint trackPoint);
/**
* Called when finish sending new track points.
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(Location)} and {@link #onSampledOutTrackPoint(Location)}.
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(TrackPoint)} and {@link #onSampledOutTrackPoint(TrackPoint)}.
*/
void onNewTrackPointsDone();
@@ -77,36 +76,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);
}
@@ -16,13 +16,7 @@
package de.dennisguse.opentracks.content;
import android.util.Log;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
/**
@@ -34,84 +28,68 @@ class TrackDataManager {
private static final String TAG = TrackDataManager.class.getSimpleName();
private final Map<TrackDataListener, EnumSet<TrackDataType>> listenerToTypesMap = new HashMap<>();
private final Set<TrackDataListener> dataListenerTracks = new HashSet<>();
private final Set<TrackDataListener> dataListenerWaypoints = new HashSet<>();
private final Set<TrackDataListener> dataListenerTrackPoints_SampledIn = new HashSet<>();
private final Set<TrackDataListener> dataListenerTrackPoints_SampledOut = new HashSet<>();
private final Map<TrackDataType, Set<TrackDataListener>> typeToListenersMap = new EnumMap<>(TrackDataType.class);
TrackDataManager() {
for (TrackDataType trackDataType : TrackDataType.values()) {
typeToListenersMap.put(trackDataType, new LinkedHashSet<TrackDataListener>());
}
void registerTrackDataListener(final TrackDataListener trackDataListener, boolean tracksTable, boolean waypointsTable, boolean trackPointsTable_SampleIn, boolean trackPointsTable_SampleOut) {
if (tracksTable) dataListenerTracks.add(trackDataListener);
if (waypointsTable) dataListenerWaypoints.add(trackDataListener);
if (trackPointsTable_SampleIn) dataListenerTrackPoints_SampledIn.add(trackDataListener);
if (trackPointsTable_SampleOut) dataListenerTrackPoints_SampledOut.add(trackDataListener);
}
/**
* Registers a listener.
*
* @param listener the listener
* @param trackDataTypes the track data types the listener is interested
*/
void registerListener(TrackDataListener listener, EnumSet<TrackDataType> trackDataTypes) {
if (listenerToTypesMap.containsKey(listener)) {
Log.w(TAG, "Tried to register a listener that is already registered. Ignore.");
return;
}
listenerToTypesMap.put(listener, trackDataTypes);
for (TrackDataType trackDataType : trackDataTypes) {
typeToListenersMap.get(trackDataType).add(listener);
}
void unregisterTrackDataListener(TrackDataListener trackDataListener) {
dataListenerTracks.add(trackDataListener);
dataListenerWaypoints.add(trackDataListener);
dataListenerTrackPoints_SampledIn.add(trackDataListener);
dataListenerTrackPoints_SampledOut.add(trackDataListener);
}
/**
* Unregisters a listener.
*
* @param listener the listener
*/
void unregisterListener(TrackDataListener listener) {
EnumSet<TrackDataType> removedTypes = listenerToTypesMap.remove(listener);
if (removedTypes == null) {
Log.w(TAG, "Tried to unregister a listener that is not registered. Ignore.");
return;
}
// Remove the listener from the typeToListenersMap
for (TrackDataType trackDataType : removedTypes) {
typeToListenersMap.get(trackDataType).remove(listener);
}
boolean hasListeners() {
return dataListenerTracks.size() + dataListenerWaypoints.size() + dataListenerTrackPoints_SampledIn.size() + dataListenerTrackPoints_SampledOut.size() > 0;
}
/**
* Gets the number of {@link TrackDataListener}.
*/
int getNumberOfListeners() {
return listenerToTypesMap.size();
Set<TrackDataListener> listener = new HashSet<>();
listener.addAll(dataListenerTracks);
listener.addAll(dataListenerWaypoints);
listener.addAll(dataListenerTrackPoints_SampledIn);
listener.addAll(dataListenerTrackPoints_SampledOut);
return listener.size();
}
/**
* Gets the track data types for a listener.
*
* @param listener the listener
*/
EnumSet<TrackDataType> getTrackDataTypes(TrackDataListener listener) {
return listenerToTypesMap.get(listener);
boolean listensForTracks(TrackDataListener listener) {
return dataListenerTracks.contains(listener);
}
/**
* Gets the listeners for a {@link TrackDataType}.
*
* @param type the type
*/
Set<TrackDataListener> getListeners(TrackDataType type) {
return typeToListenersMap.get(type);
boolean listensForWaypoints(TrackDataListener listener) {
return dataListenerWaypoints.contains(listener);
}
/**
* Gets all the registered {@link TrackDataType}.
*/
EnumSet<TrackDataType> getRegisteredTrackDataTypes() {
EnumSet<TrackDataType> types = EnumSet.noneOf(TrackDataType.class);
for (EnumSet<TrackDataType> value : listenerToTypesMap.values()) {
types.addAll(value);
}
return types;
boolean listensForTrackPoints_SampledIn(TrackDataListener listener) {
return dataListenerTrackPoints_SampledIn.contains(listener);
}
boolean listensForTrackPoints_SampledOut(TrackDataListener listener) {
return dataListenerTrackPoints_SampledOut.contains(listener);
}
Set<TrackDataListener> getListenerTracks() {
return dataListenerTracks;
}
Set<TrackDataListener> getListenerWaypoints() {
return dataListenerWaypoints;
}
Set<TrackDataListener> getListenerTrackPoints_SampledIn() {
return dataListenerTrackPoints_SampledIn;
}
Set<TrackDataListener> getListenerTrackPoints_SampledOut() {
return dataListenerTrackPoints_SampledOut;
}
}
@@ -1,30 +0,0 @@
/*
* Copyright 2011 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.content;
/**
* Types of track data.
*
* @author Jimmy Shih
*/
public enum TrackDataType {
TRACKS_TABLE, // tracks table changes
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
}
@@ -16,12 +16,12 @@
package de.dennisguse.opentracks.content.data;
import android.location.Location;
import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* A track.
@@ -36,16 +36,12 @@ public class Track {
private String description = "";
private String category = "";
/*
* The number of location points (present even if the points themselves are not loaded)
*/
private int numberOfPoints = 0;
private String icon = "";
private TripStatistics tripStatistics = new TripStatistics();
private TrackStatistics trackStatistics = new TrackStatistics();
// Location points (which may not have been loaded)
private List<Location> locations = new ArrayList<>();
private List<TrackPoint> trackPoints = new ArrayList<>();
public Track() {
}
@@ -82,14 +78,6 @@ public class Track {
this.category = category;
}
public int getNumberOfPoints() {
return numberOfPoints;
}
public void setNumberOfPoints(int numberOfPoints) {
this.numberOfPoints = numberOfPoints;
}
public String getIcon() {
return icon;
}
@@ -98,23 +86,24 @@ public class Track {
this.icon = icon;
}
public TripStatistics getTripStatistics() {
return tripStatistics;
public TrackStatistics getTrackStatistics() {
return trackStatistics;
}
public void setTripStatistics(TripStatistics tripStatistics) {
this.tripStatistics = tripStatistics;
public void setTrackStatistics(TrackStatistics trackStatistics) {
this.trackStatistics = trackStatistics;
}
public void addLocation(Location location) {
locations.add(location);
@VisibleForTesting
public void addTrackPoint(TrackPoint location) {
trackPoints.add(location);
}
public List<Location> getLocations() {
return locations;
public List<TrackPoint> getTrackPoints() {
return trackPoints;
}
public void setLocations(ArrayList<Location> locations) {
this.locations = locations;
public void setTrackPoints(ArrayList<TrackPoint> trackPoints) {
this.trackPoints = trackPoints;
}
}
@@ -16,6 +16,9 @@
package de.dennisguse.opentracks.content.data;
import android.location.Location;
import android.location.LocationManager;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
@@ -24,17 +27,67 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
*
* @author Sandor Dornbush
*/
public class TrackPoint extends Location {
public class TrackPoint {
private SensorDataSet sensorDataSet = null;
private final Location location;
public TrackPoint(Location location, SensorDataSet sensorDataSet) {
super(location);
public TrackPoint() {
this.location = new Location("");
}
public TrackPoint(@NonNull Location location) {
this.location = location;
}
public TrackPoint(@NonNull Location location, SensorDataSet sensorDataSet) {
this.location = location;
this.sensorDataSet = sensorDataSet;
}
public TrackPoint(String provider) {
super(provider);
public TrackPoint(@NonNull TrackPoint trackPoint, SensorDataSet sensorDataSet) {
this.location = trackPoint.getLocation();
this.sensorDataSet = sensorDataSet;
}
private SensorDataSet sensorDataSet = null;
public TrackPoint(double latitude, double longitude, Double altitude, long time) {
location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(latitude);
location.setLongitude(longitude);
if (altitude != null) {
location.setAltitude(altitude);
}
location.setTime(time);
}
public static TrackPoint createPause() {
return createPauseWithTime(System.currentTimeMillis());
}
public static TrackPoint createPauseWithTime(long time) {
Location pause = new Location(LocationManager.GPS_PROVIDER);
pause.setLongitude(0);
pause.setLatitude(TrackPointsColumns.PAUSE_LATITUDE);
pause.setTime(time);
return new TrackPoint(pause);
}
public static TrackPoint createResume() {
return createResumeWithTime(System.currentTimeMillis());
}
public static TrackPoint createResumeWithTime(long time) {
Location resume = new Location(LocationManager.GPS_PROVIDER);
resume.setLongitude(0);
resume.setLatitude(TrackPointsColumns.RESUME_LATITUDE);
resume.setTime(time);
return new TrackPoint(resume);
}
public @NonNull
Location getLocation() {
return location;
}
public SensorDataSet getSensorDataSet() {
@@ -45,8 +98,87 @@ public class TrackPoint extends Location {
this.sensorDataSet = sensorDataSet;
}
public void reset() {
super.reset();
sensorDataSet = null;
public double getLatitude() {
return location.getLatitude();
}
public void setLatitude(double latitude) {
location.setLatitude(latitude);
}
public double getLongitude() {
return location.getLongitude();
}
public void setLongitude(double longitude) {
location.setLongitude(longitude);
}
public long getTime() {
return location.getTime();
}
public void setTime(long time) {
location.setTime(time);
}
public boolean hasAltitude() {
return location.hasAltitude();
}
public double getAltitude() {
return location.getAltitude();
}
public void setAltitude(double altitude) {
location.setAltitude(altitude);
}
public boolean hasSpeed() {
return location.hasSpeed();
}
public float getSpeed() {
return location.getSpeed();
}
public void setSpeed(float speed) {
location.setSpeed(speed);
}
public boolean hasBearing() {
return location.hasBearing();
}
public float getBearing() {
return location.getBearing();
}
public void setBearing(float bearing) {
location.setBearing(bearing);
}
public boolean hasAccuracy() {
return location.hasAccuracy();
}
public float getAccuracy() {
return location.getAccuracy();
}
public void setAccuracy(float horizontalAccuracy) {
location.setAccuracy(horizontalAccuracy);
}
public float distanceTo(@NonNull TrackPoint dest) {
return location.distanceTo(dest.getLocation());
}
public float bearingTo(@NonNull TrackPoint dest) {
return location.bearingTo(dest.getLocation());
}
public float bearingTo(@NonNull Location dest) {
return location.bearingTo(dest);
}
}
@@ -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 + "/" + TABLE_NAME);
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";
@@ -59,5 +64,7 @@ public interface TrackPointsColumns extends BaseColumns {
+ BEARING + " FLOAT, "
+ SENSOR_HEARTRATE + " FLOAT, "
+ SENSOR_CADENCE + " FLOAT, "
+ SENSOR_POWER + " FLOAT);";
+ SENSOR_POWER + " FLOAT)";
String CREATE_TABLE_INDEX = "CREATE INDEX " + TABLE_NAME + "_" + TRACKID + "_index ON " + TABLE_NAME + "(" + TRACKID + ")";
}
@@ -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.
@@ -38,34 +38,20 @@ public interface TracksColumns extends BaseColumns {
String NAME = "name"; // track name
String DESCRIPTION = "description"; // track description
String CATEGORY = "category"; // track activity type
@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
@Deprecated
String NUMPOINTS = "numpoints"; // number of track points //TODO UNUSED
String TOTALDISTANCE = "totaldistance"; // total distance
String TOTALTIME = "totaltime"; // total time
String MOVINGTIME = "movingtime"; // moving time
@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
String ELEVATIONGAIN = "elevationgain"; // elevation gain
String MINGRADE = "mingrade"; // minimum grade
String MAXGRADE = "maxgrade"; // maximum grade
String ICON = "icon"; // track activity type icon
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" // table
@@ -73,25 +59,17 @@ public interface TracksColumns extends BaseColumns {
+ NAME + " TEXT, " // name
+ DESCRIPTION + " TEXT, " // description
+ CATEGORY + " TEXT, " // category
+ STARTID + " INTEGER, " // start id
+ STOPID + " INTEGER, " // stop id
+ STARTTIME + " INTEGER, " // start time
+ STOPTIME + " INTEGER, " // stop time
+ NUMPOINTS + " INTEGER, " // num points
+ TOTALDISTANCE + " FLOAT, " // total distance
+ TOTALTIME + " INTEGER, " // total time
+ MOVINGTIME + " INTEGER, " // moving time
+ MINLAT + " INTEGER, " // min latitude
+ MAXLAT + " INTEGER, " // max latitude
+ MINLON + " INTEGER, " // min longitude
+ MAXLON + " INTEGER, " // max longitude
+ AVGSPEED + " FLOAT, " // average speed
+ AVGMOVINGSPEED + " FLOAT, " // average moving speed
+ MAXSPEED + " FLOAT, " // max speed
+ MINELEVATION + " FLOAT, " // min elevation
+ MAXELEVATION + " FLOAT, " // max elevation
+ ELEVATIONGAIN + " FLOAT, " // elevation gain
+ MINGRADE + " FLOAT, " // min grade
+ MAXGRADE + " FLOAT, " // max grade
+ ICON + " TEXT);"; // icon
+ ICON + " TEXT)"; // icon
}
@@ -19,6 +19,9 @@ package de.dennisguse.opentracks.content.data;
import android.location.Location;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
/**
* A waypoint.
*
@@ -38,10 +41,15 @@ public final class Waypoint {
private Location location = null;
private String photoUrl = "";
@VisibleForTesting
public Waypoint() {
}
public Waypoint(String name, String description, String category, String icon, long trackId, double length, long duration, Location location, String photoUrl) {
public Waypoint(Location location) {
this.location = location;
}
public Waypoint(String name, String description, String category, String icon, long trackId, double length, long duration, @NonNull Location location, String photoUrl) {
this.name = name;
this.description = description;
this.category = category;
@@ -117,14 +125,11 @@ public final class Waypoint {
this.duration = duration;
}
public Location getLocation() {
public @NonNull
Location getLocation() {
return location;
}
public void setLocation(Location location) {
this.location = location;
}
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.
@@ -40,49 +40,17 @@ public interface WaypointsColumns extends BaseColumns {
String CATEGORY = "category"; // waypoint category
String ICON = "icon"; // waypoint icon
String TRACKID = "trackid"; // track id
@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
@Deprecated
String STARTID = "startid"; // start track point id // TODO UNUSED
@Deprecated
String STOPID = "stopid"; // stop track point id // TODO UNUSED
String DURATION = "duration"; // total duration of the track from the beginning until now
String LONGITUDE = "longitude"; // longitude
String LATITUDE = "latitude"; // latitude
String TIME = "time"; // time
String ALTITUDE = "elevation"; // altitude
String ACCURACY = "accuracy"; // accuracy
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
@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
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
@@ -92,30 +60,16 @@ public interface WaypointsColumns extends BaseColumns {
+ CATEGORY + " TEXT, "
+ ICON + " TEXT, "
+ TRACKID + " INTEGER, "
+ TYPE + " INTEGER, "
+ LENGTH + " FLOAT, "
+ DURATION + " INTEGER, "
+ STARTTIME + " INTEGER, "
+ STARTID + " INTEGER, "
+ STOPID + " INTEGER, "
+ LONGITUDE + " INTEGER, "
+ LATITUDE + " INTEGER, "
+ TIME + " INTEGER, "
+ ALTITUDE + " FLOAT, "
+ ACCURACY + " FLOAT, "
+ SPEED + " FLOAT, "
+ BEARING + " FLOAT, "
+ TOTALDISTANCE + " FLOAT, "
+ TOTALTIME + " INTEGER, "
+ MOVINGTIME + " INTEGER, "
+ AVGSPEED + " FLOAT, "
+ AVGMOVINGSPEED + " FLOAT, "
+ MAXSPEED + " FLOAT, "
+ MINELEVATION + " FLOAT, "
+ MAXELEVATION + " FLOAT, "
+ ELEVATIONGAIN + " FLOAT, "
+ MINGRADE + " FLOAT, "
+ MAXGRADE + " FLOAT, "
+ PHOTOURL + " TEXT"
+ ");";
+ ")";
String CREATE_TABLE_INDEX = "CREATE INDEX " + TABLE_NAME + "_" + TRACKID + "_index ON " + TABLE_NAME + "(" + TRACKID + ")";
}
@@ -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,7 +30,6 @@ 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;
@@ -42,7 +41,7 @@ import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.FileUtils;
/**
@@ -59,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;
private static final String ID_SEPARATOR = ",";
private final IContentResolver contentResolver;
private int defaultCursorBatchSize = 2000;
@@ -109,11 +109,8 @@ 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);
@@ -121,12 +118,10 @@ public class ContentProviderUtils {
int minElevationIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINELEVATION);
int maxElevationIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXELEVATION);
int elevationGainIndex = cursor.getColumnIndexOrThrow(TracksColumns.ELEVATIONGAIN);
int minGradeIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINGRADE);
int maxGradeIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXGRADE);
int iconIndex = cursor.getColumnIndexOrThrow(TracksColumns.ICON);
Track track = new Track();
TripStatistics tripStatistics = track.getTripStatistics();
TrackStatistics trackStatistics = track.getTrackStatistics();
if (!cursor.isNull(idIndex)) {
track.setId(cursor.getLong(idIndex));
}
@@ -140,40 +135,31 @@ public class ContentProviderUtils {
track.setCategory(cursor.getString(categoryIndex));
}
if (!cursor.isNull(startTimeIndex)) {
tripStatistics.setStartTime(cursor.getLong(startTimeIndex));
trackStatistics.setStartTime_ms(cursor.getLong(startTimeIndex));
}
if (!cursor.isNull(stopTimeIndex)) {
tripStatistics.setStopTime(cursor.getLong(stopTimeIndex));
}
if (!cursor.isNull(numPointsIndex)) {
track.setNumberOfPoints(cursor.getInt(numPointsIndex));
trackStatistics.setStopTime_ms(cursor.getLong(stopTimeIndex));
}
if (!cursor.isNull(totalDistanceIndex)) {
tripStatistics.setTotalDistance(cursor.getFloat(totalDistanceIndex));
trackStatistics.setTotalDistance(cursor.getFloat(totalDistanceIndex));
}
if (!cursor.isNull(totalTimeIndex)) {
tripStatistics.setTotalTime(cursor.getLong(totalTimeIndex));
trackStatistics.setTotalTime(cursor.getLong(totalTimeIndex));
}
if (!cursor.isNull(movingTimeIndex)) {
tripStatistics.setMovingTime(cursor.getLong(movingTimeIndex));
trackStatistics.setMovingTime(cursor.getLong(movingTimeIndex));
}
if (!cursor.isNull(maxSpeedIndex)) {
tripStatistics.setMaxSpeed(cursor.getFloat(maxSpeedIndex));
trackStatistics.setMaxSpeed(cursor.getFloat(maxSpeedIndex));
}
if (!cursor.isNull(minElevationIndex)) {
tripStatistics.setMinElevation(cursor.getFloat(minElevationIndex));
trackStatistics.setMinElevation(cursor.getFloat(minElevationIndex));
}
if (!cursor.isNull(maxElevationIndex)) {
tripStatistics.setMaxElevation(cursor.getFloat(maxElevationIndex));
trackStatistics.setMaxElevation(cursor.getFloat(maxElevationIndex));
}
if (!cursor.isNull(elevationGainIndex)) {
tripStatistics.setTotalElevationGain(cursor.getFloat(elevationGainIndex));
}
if (!cursor.isNull(minGradeIndex)) {
tripStatistics.setMinGrade(cursor.getFloat(minGradeIndex));
}
if (!cursor.isNull(maxGradeIndex)) {
tripStatistics.setMaxGrade(cursor.getFloat(maxGradeIndex));
trackStatistics.setTotalElevationGain(cursor.getFloat(elevationGainIndex));
}
if (!cursor.isNull(iconIndex)) {
track.setIcon(cursor.getString(iconIndex));
@@ -185,13 +171,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(context);
deleteDirectoryRecurse(context, dir);
deleteDirectoryRecurse(dir);
}
/**
@@ -209,18 +195,17 @@ public class ContentProviderUtils {
/**
* 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) {
String where = TrackPointsColumns.TRACKID + "=?";
String[] selectionArgs = new String[]{Long.toString(trackId)};
contentResolver.delete(TrackPointsColumns.CONTENT_URI, where, selectionArgs);
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(context, trackId));
deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
}
/**
@@ -228,10 +213,10 @@ public class ContentProviderUtils {
*
* @param dir the directory
*/
private void deleteDirectoryRecurse(Context context, File dir) {
if (dir.exists() && dir.isDirectory()) {
private void deleteDirectoryRecurse(File dir) {
if (dir != null && dir.exists() && dir.isDirectory()) {
for (File child : dir.listFiles()) {
deleteDirectoryRecurse(context, child);
deleteDirectoryRecurse(child);
}
dir.delete();
}
@@ -242,9 +227,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 {
@@ -259,7 +245,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);
@@ -278,7 +264,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);
}
@@ -295,7 +281,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);
}
/**
@@ -322,7 +308,7 @@ public class ContentProviderUtils {
private ContentValues createContentValues(Track track) {
ContentValues values = new ContentValues();
TripStatistics tripStatistics = track.getTripStatistics();
TrackStatistics trackStatistics = track.getTrackStatistics();
// Value < 0 indicates no id is available
if (track.getId() >= 0) {
@@ -331,36 +317,22 @@ public class ContentProviderUtils {
values.put(TracksColumns.NAME, track.getName());
values.put(TracksColumns.DESCRIPTION, track.getDescription());
values.put(TracksColumns.CATEGORY, track.getCategory());
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.AVGSPEED, tripStatistics.getAverageSpeed());
values.put(TracksColumns.AVGMOVINGSPEED, tripStatistics.getAverageMovingSpeed());
values.put(TracksColumns.MAXSPEED, tripStatistics.getMaxSpeed());
values.put(TracksColumns.MINELEVATION, tripStatistics.getMinElevation());
values.put(TracksColumns.MAXELEVATION, tripStatistics.getMaxElevation());
values.put(TracksColumns.ELEVATIONGAIN, tripStatistics.getTotalElevationGain());
values.put(TracksColumns.MINGRADE, tripStatistics.getMinGrade());
values.put(TracksColumns.MAXGRADE, tripStatistics.getMaxGrade());
values.put(TracksColumns.STARTTIME, trackStatistics.getStartTime_ms());
values.put(TracksColumns.STOPTIME, trackStatistics.getStopTime_ms());
values.put(TracksColumns.TOTALDISTANCE, trackStatistics.getTotalDistance());
values.put(TracksColumns.TOTALTIME, trackStatistics.getTotalTime());
values.put(TracksColumns.MOVINGTIME, trackStatistics.getMovingTime());
values.put(TracksColumns.AVGSPEED, trackStatistics.getAverageSpeed());
values.put(TracksColumns.AVGMOVINGSPEED, trackStatistics.getAverageMovingSpeed());
values.put(TracksColumns.MAXSPEED, trackStatistics.getMaxSpeed());
values.put(TracksColumns.MINELEVATION, trackStatistics.getMinElevation());
values.put(TracksColumns.MAXELEVATION, trackStatistics.getMaxElevation());
values.put(TracksColumns.ELEVATIONGAIN, trackStatistics.getTotalElevationGain());
values.put(TracksColumns.ICON, track.getIcon());
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.
@@ -381,11 +353,28 @@ public class ContentProviderUtils {
int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
int altitudeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.ALTITUDE);
int accuracyIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.ACCURACY);
int speedIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.SPEED);
int bearingIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.BEARING);
int photoUrlIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.PHOTOURL);
Waypoint waypoint = new Waypoint();
Location location = new Location("");
if (!cursor.isNull(longitudeIndex) && !cursor.isNull(latitudeIndex)) {
location.setLongitude(((double) cursor.getInt(longitudeIndex)) / 1E6);
location.setLatitude(((double) cursor.getInt(latitudeIndex)) / 1E6);
}
if (!cursor.isNull(timeIndex)) {
location.setTime(cursor.getLong(timeIndex));
}
if (!cursor.isNull(altitudeIndex)) {
location.setAltitude(cursor.getFloat(altitudeIndex));
}
if (!cursor.isNull(accuracyIndex)) {
location.setAccuracy(cursor.getFloat(accuracyIndex));
}
if (!cursor.isNull(bearingIndex)) {
location.setBearing(cursor.getFloat(bearingIndex));
}
Waypoint waypoint = new Waypoint(location);
if (!cursor.isNull(idIndex)) {
waypoint.setId(cursor.getLong(idIndex));
@@ -412,28 +401,6 @@ public class ContentProviderUtils {
waypoint.setDuration(cursor.getLong(durationIndex));
}
Location location = new Location("");
if (!cursor.isNull(longitudeIndex) && !cursor.isNull(latitudeIndex)) {
location.setLongitude(((double) cursor.getInt(longitudeIndex)) / 1E6);
location.setLatitude(((double) cursor.getInt(latitudeIndex)) / 1E6);
}
if (!cursor.isNull(timeIndex)) {
location.setTime(cursor.getLong(timeIndex));
}
if (!cursor.isNull(altitudeIndex)) {
location.setAltitude(cursor.getFloat(altitudeIndex));
}
if (!cursor.isNull(accuracyIndex)) {
location.setAccuracy(cursor.getFloat(accuracyIndex));
}
if (!cursor.isNull(speedIndex)) {
location.setSpeed(cursor.getFloat(speedIndex));
}
if (!cursor.isNull(bearingIndex)) {
location.setBearing(cursor.getFloat(bearingIndex));
}
waypoint.setLocation(location);
if (!cursor.isNull(photoUrlIndex)) {
waypoint.setPhotoUrl(cursor.getString(photoUrlIndex));
}
@@ -448,7 +415,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();
@@ -464,26 +431,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.
@@ -645,9 +592,6 @@ public class ContentProviderUtils {
if (location.hasAccuracy()) {
values.put(WaypointsColumns.ACCURACY, location.getAccuracy());
}
if (location.hasSpeed()) {
values.put(WaypointsColumns.SPEED, location.getSpeed());
}
if (location.hasBearing()) {
values.put(WaypointsColumns.BEARING, location.getBearing());
}
@@ -677,34 +621,63 @@ public class ContentProviderUtils {
}
/**
* Inserts multiple track points.
* Fills a {@link TrackPoint} from a cursor.
*
* @param locations an array of locations
* @param length the number of locations (from the beginning of the array) to
* insert, or -1 for all of them
* @param trackId the track id
* @return the number of points inserted
* @param cursor the cursor pointing to a trackPoint.
* @param indexes the cached track points indexes
*/
public int bulkInsertTrackPoint(Location[] locations, int length, long trackId) {
if (length == -1) {
length = locations.length;
static TrackPoint fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes) {
TrackPoint trackPoint = new TrackPoint();
if (!cursor.isNull(indexes.longitudeIndex)) {
trackPoint.setLongitude(((double) cursor.getInt(indexes.longitudeIndex)) / 1E6);
}
ContentValues[] values = new ContentValues[length];
for (int i = 0; i < length; i++) {
values[i] = createContentValues(locations[i], trackId);
if (!cursor.isNull(indexes.latitudeIndex)) {
trackPoint.setLatitude(((double) cursor.getInt(indexes.latitudeIndex)) / 1E6);
}
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI, values);
if (!cursor.isNull(indexes.timeIndex)) {
trackPoint.setTime(cursor.getLong(indexes.timeIndex));
}
if (!cursor.isNull(indexes.altitudeIndex)) {
trackPoint.setAltitude(cursor.getFloat(indexes.altitudeIndex));
}
if (!cursor.isNull(indexes.accuracyIndex)) {
trackPoint.setAccuracy(cursor.getFloat(indexes.accuracyIndex));
}
if (!cursor.isNull(indexes.speedIndex)) {
trackPoint.setSpeed(cursor.getFloat(indexes.speedIndex));
}
if (!cursor.isNull(indexes.bearingIndex)) {
trackPoint.setBearing(cursor.getFloat(indexes.bearingIndex));
}
float heartRate = cursor.isNull(indexes.sensorHeartRateIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorHeartRateIndex);
float cadence = cursor.isNull(indexes.sensorCadenceIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorCadenceIndex);
float power = cursor.isNull(indexes.sensorPowerIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorPowerIndex);
trackPoint.setSensorDataSet(new SensorDataSet(heartRate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, trackPoint.getTime()));
return trackPoint;
}
/**
* Creates a location object from a cursor.
* Inserts multiple trackPoints points.
*
* @param cursor the cursor pointing to the location
* @param trackPoints an array of trackPoints
* @param length the number of trackPoints (from the beginning of the array) to
* insert, or -1 for all of them
* @param trackId the trackPoints id
* @return the number of points inserted
*/
public Location createTrackPoint(Cursor cursor) {
Location location = new TrackPoint("");
fillTrackPoint(cursor, new CachedTrackPointsIndexes(cursor), location);
return location;
public int bulkInsertTrackPoint(TrackPoint[] trackPoints, int length, long trackId) {
if (length == -1) {
length = trackPoints.length;
}
ContentValues[] values = new ContentValues[length];
for (int i = 0; i < length; i++) {
values[i] = createContentValues(trackPoints[i], trackId);
}
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
}
/**
@@ -777,21 +750,12 @@ public class ContentProviderUtils {
}
/**
* Gets the last valid location for a track.
* Returns null if it doesn't exist.
* Creates a {@link TrackPoint} object from a cursor.
*
* @param trackId the track id
* @param cursor the cursor pointing to the location
*/
@Deprecated
public Location getLastValidTrackPoint(long trackId) {
if (trackId < 0) {
return null;
}
String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID + ") from "
+ TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND "
+ TrackPointsColumns.LATITUDE + "<=" + MAX_LATITUDE + ")";
String[] selectionArgs = new String[]{Long.toString(trackId)};
return findTrackPointBy(selection, selectionArgs);
public TrackPoint createTrackPoint(Cursor cursor) {
return fillTrackPoint(cursor, new CachedTrackPointsIndexes(cursor));
}
/**
@@ -829,203 +793,91 @@ public class ContentProviderUtils {
return getTrackPointCursor(null, selection, selectionArgs, sortOrder);
}
/**
* 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.
* Gets the last valid location for a track.
* Returns null if it doesn't exist.
*
* @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
* @param trackId the track id
*/
public LocationIterator getTrackPointLocationIterator(final long trackId,
final long startTrackPointId, final boolean descending,
final LocationFactory locationFactory) {
if (locationFactory == null) {
throw new IllegalArgumentException("locationFactory is null");
@Deprecated
public TrackPoint getLastValidTrackPoint(long trackId) {
if (trackId < 0) {
return 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();
}
};
String selection = TrackPointsColumns._ID + "=(select max(" + TrackPointsColumns._ID + ") from "
+ TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND "
+ TrackPointsColumns.LATITUDE + "<=" + MAX_LATITUDE + ")";
String[] selectionArgs = new String[]{Long.toString(trackId)};
return findTrackPointBy(selection, selectionArgs);
}
/**
* Inserts a track point.
*
* @param location the location
* @param trackPoint the trackPoint
* @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));
public Uri insertTrackPoint(TrackPoint trackPoint, long trackId) {
return contentResolver.insert(TrackPointsColumns.CONTENT_URI_BY_ID, createContentValues(trackPoint, trackId));
}
/**
* Creates the {@link ContentValues} for a {@link Location}.
* Creates the {@link ContentValues} for a {@link TrackPoint}.
*
* @param location the location
* @param trackPoint the trackPoint
* @param trackId the track id
*/
private ContentValues createContentValues(Location location, long trackId) {
private ContentValues createContentValues(TrackPoint trackPoint, 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.LONGITUDE, (int) (trackPoint.getLongitude() * 1E6));
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.getLatitude() * 1E6));
values.put(TrackPointsColumns.TIME, location.getTime());
if (location.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, location.getAltitude());
values.put(TrackPointsColumns.TIME, trackPoint.getTime());
if (trackPoint.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude());
}
if (location.hasAccuracy()) {
values.put(TrackPointsColumns.ACCURACY, location.getAccuracy());
if (trackPoint.hasAccuracy()) {
values.put(TrackPointsColumns.ACCURACY, trackPoint.getAccuracy());
}
if (location.hasSpeed()) {
values.put(TrackPointsColumns.SPEED, location.getSpeed());
if (trackPoint.hasSpeed()) {
values.put(TrackPointsColumns.SPEED, trackPoint.getSpeed());
}
if (location.hasBearing()) {
values.put(TrackPointsColumns.BEARING, location.getBearing());
if (trackPoint.hasBearing()) {
values.put(TrackPointsColumns.BEARING, trackPoint.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());
}
SensorDataSet sensorDataSet = trackPoint.getSensorDataSet();
if (sensorDataSet != null && sensorDataSet.hasHeartRate()) {
values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.getSensorDataSet().getHeartRate());
}
if (sensorDataSet != null && sensorDataSet.hasCadence()) {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getSensorDataSet().getCadence());
}
if (sensorDataSet != null && sensorDataSet.hasPower()) {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getSensorDataSet().getPower());
}
return values;
}
/**
* Fills a track point from a cursor.
* 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.
* When done with iteration, {@link TrackPointIterator#close()} must be called.
*
* @param cursor the cursor pointing to a location.
* @param indexes the cached track points indexes
* @param location the track point
* @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)
*/
private void fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes, Location location) {
location.reset();
if (!cursor.isNull(indexes.longitudeIndex)) {
location.setLongitude(((double) cursor.getInt(indexes.longitudeIndex)) / 1E6);
}
if (!cursor.isNull(indexes.latitudeIndex)) {
location.setLatitude(((double) cursor.getInt(indexes.latitudeIndex)) / 1E6);
}
if (!cursor.isNull(indexes.timeIndex)) {
location.setTime(cursor.getLong(indexes.timeIndex));
}
if (!cursor.isNull(indexes.altitudeIndex)) {
location.setAltitude(cursor.getFloat(indexes.altitudeIndex));
}
if (!cursor.isNull(indexes.accuracyIndex)) {
location.setAccuracy(cursor.getFloat(indexes.accuracyIndex));
}
if (!cursor.isNull(indexes.speedIndex)) {
location.setSpeed(cursor.getFloat(indexes.speedIndex));
}
if (!cursor.isNull(indexes.bearingIndex)) {
location.setBearing(cursor.getFloat(indexes.bearingIndex));
}
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);
float power = cursor.isNull(indexes.sensorPowerIndex) ? SensorDataSet.DATA_UNAVAILABLE : cursor.getFloat(indexes.sensorPowerIndex);
sensorDataSetLocation.setSensorDataSet(new SensorDataSet(heartRate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, location.getTime()));
}
public TrackPointIterator getTrackPointLocationIterator(final long trackId, final long startTrackPointId, final boolean descending) {
return new TrackPointIterator(this, trackId, startTrackPointId, descending);
}
private Location findTrackPointBy(String selection, String[] selectionArgs) {
private TrackPoint findTrackPointBy(String selection, String[] selectionArgs) {
try (Cursor cursor = getTrackPointCursor(null, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToNext()) {
return createTrackPoint(cursor);
@@ -1043,7 +895,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;
}
/**
@@ -1057,33 +913,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,27 +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_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.TABLE_NAME + "/#", UrlType.TRACKPOINTS_BY_TRACKID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME, UrlType.TRACKS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/#", UrlType.TRACKS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.TABLE_NAME, UrlType.WAYPOINTS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, WaypointsColumns.TABLE_NAME + "/#", UrlType.WAYPOINTS_BY_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
@@ -81,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) {
@@ -134,7 +131,6 @@ public abstract class CustomContentProvider extends ContentProvider {
case TRACKPOINTS:
return TrackPointsColumns.CONTENT_TYPE;
case TRACKPOINTS_BY_ID:
return TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKPOINTS_BY_TRACKID:
return TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKS:
@@ -201,11 +197,11 @@ public abstract class CustomContentProvider extends ContentProvider {
break;
case TRACKPOINTS_BY_ID:
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere(TrackPointsColumns._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 + "=" + url.getPathSegments().get(1));
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
break;
case TRACKS:
queryBuilder.setTables(TracksColumns.TABLE_NAME);
@@ -213,7 +209,7 @@ public abstract class CustomContentProvider extends ContentProvider {
break;
case TRACKS_BY_ID:
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere(TracksColumns._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);
@@ -221,7 +217,7 @@ public abstract class CustomContentProvider extends ContentProvider {
break;
case WAYPOINTS_BY_ID:
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
queryBuilder.appendWhere(WaypointsColumns._ID + "=" + url.getPathSegments().get(1));
queryBuilder.appendWhere(WaypointsColumns._ID + "=" + ContentUris.parseId(url));
break;
default:
throw new IllegalArgumentException("Unknown url " + url);
@@ -243,7 +239,7 @@ public abstract class CustomContentProvider extends ContentProvider {
break;
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 + ")";
}
@@ -254,7 +250,7 @@ public abstract class CustomContentProvider extends ContentProvider {
break;
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 + ")";
}
@@ -265,7 +261,7 @@ public abstract class CustomContentProvider extends ContentProvider {
break;
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 + ")";
}
@@ -290,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);
}
/**
@@ -329,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);
}
@@ -381,31 +384,4 @@ public abstract class CustomContentProvider extends ContentProvider {
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,145 @@
package de.dennisguse.opentracks.content.provider;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
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
class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
@VisibleForTesting
private static final int DATABASE_VERSION = 25;
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
@VisibleForTesting
private 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);
}
@VisibleForTesting
public CustomSQLiteOpenHelper(Context context, String databaseName, int databaseVersion) {
super(context, databaseName, null, databaseVersion);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(TrackPointsColumns.CREATE_TABLE);
db.execSQL(TrackPointsColumns.CREATE_TABLE_INDEX);
db.execSQL(TracksColumns.CREATE_TABLE);
db.execSQL(WaypointsColumns.CREATE_TABLE);
db.execSQL(WaypointsColumns.CREATE_TABLE_INDEX);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
for (int toVersion = oldVersion + 1; toVersion <= newVersion; toVersion++) {
Log.i(TAG, "Upgrade from " + oldVersion + " to " + toVersion);
switch (toVersion) {
case 24:
upgradeFrom23to24(db);
break;
case 25:
upgradeFrom24to25(db);
break;
default:
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
}
}
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
for (int toVersion = oldVersion - 1; toVersion >= newVersion; toVersion--) {
Log.i(TAG, "Downgrade from " + oldVersion + " to " + toVersion);
switch (toVersion) {
case 23:
downgradeFrom24to23(db);
break;
case 24:
downgradeFrom25to24(db);
break;
default:
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
}
}
}
/**
* Upgrade from database version 23 (waypoints, tracks): remove unused columns.
* SQLite3 does not support drop columns; therefore new tables are created and data is copied.
*/
private void upgradeFrom23to24(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("ALTER TABLE tracks RENAME TO tracks_old");
db.execSQL("CREATE TABLE tracks (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, starttime INTEGER, stoptime INTEGER, numpoints INTEGER, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, icon TEXT)");
db.execSQL("INSERT INTO tracks SELECT _id, name, description, category, starttime, stoptime, numpoints, totaldistance, totaltime, movingtime, avgspeed, avgmovingspeed, maxspeed, minelevation, maxelevation, elevationgain, icon FROM tracks_old");
db.execSQL("DROP TABLE tracks_old");
db.execSQL("ALTER TABLE waypoints RENAME TO waypoints_old");
db.execSQL("CREATE TABLE waypoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, icon TEXT, trackid INTEGER, length FLOAT, duration INTEGER, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, bearing FLOAT, photoUrl TEXT)");
db.execSQL("INSERT INTO waypoints SELECT _id, name, description, category, icon, trackid, length, duration, longitude, latitude, time, elevation, accuracy, bearing, photoUrl FROM waypoints_old");
db.execSQL("DROP TABLE waypoints_old");
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom24to23(SQLiteDatabase db) {
//Not needed as the delete columns did not contain any data
db.beginTransaction();
db.execSQL("ALTER TABLE tracks RENAME TO tracks_old");
db.execSQL("CREATE TABLE tracks (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, starttime INTEGER, stoptime INTEGER, numpoints INTEGER, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, mingrade FLOAT, maxgrade FLOAT, icon TEXT)");
db.execSQL("INSERT INTO tracks SELECT _id, name, description, category, starttime, stoptime, numpoints, totaldistance, totaltime, movingtime, avgspeed, avgmovingspeed, maxspeed, minelevation, maxelevation, elevationgain, 0, 0, icon FROM tracks_old");
db.execSQL("DROP TABLE tracks_old");
db.execSQL("ALTER TABLE waypoints RENAME TO waypoints_old");
db.execSQL("CREATE TABLE waypoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, icon TEXT, trackid INTEGER, type INTEGER, length FLOAT, duration INTEGER, starttime INTEGER, startid INTEGER, stopid INTEGER, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, mingrade FLOAT, maxgrade FLOAT, photoUrl TEXT)");
db.execSQL("INSERT INTO waypoints SELECT _id, name, description, category, icon, trackid, 0, length, duration, 0, 0, 0, longitude, latitude, time, elevation, accuracy, 0, bearing, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, photoUrl FROM waypoints_old");
db.execSQL("DROP TABLE waypoints_old");
db.setTransactionSuccessful();
db.endTransaction();
}
private void upgradeFrom24to25(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.execSQL("CREATE INDEX waypoints_trackid_index ON waypoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom25to24(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("DROP INDEX trackpoints_trackid_index");
db.execSQL("DROP INDEX waypoints_trackid_index");
db.setTransactionSuccessful();
db.endTransaction();
}
}
@@ -1,4 +1,4 @@
package de.dennisguse.opentracks.content;
package de.dennisguse.opentracks.content.provider;
import android.content.Context;
import android.content.UriMatcher;
@@ -19,6 +19,7 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import de.dennisguse.opentracks.android.IContentResolver;
import de.dennisguse.opentracks.content.data.Track;
@@ -42,7 +43,7 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
public static final String TAG = ShareContentProvider.class.getSimpleName();
private static final String TAG = ShareContentProvider.class.getSimpleName();
private static final int URI_GPX = 0;
private static final int URI_KML_ONLY = 1;
@@ -58,30 +59,30 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
private static final String TRACKID_DELIMITER = "_";
static {
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.GPX.getName() + "/*", URI_GPX);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.GPX.getName() + "/*/*", URI_GPX);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_ONLY_TRACK.getName() + "/*", URI_KML_ONLY);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_WITH_TRACKDETAIL.getName() + "/*", URI_KML_WITH_TRACKDETAIL);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*", URI_KML_WITH_TRACKDETAIL_SENSORDATA);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_ONLY_TRACK.getName() + "/*", URI_KMZ_ONLY_TRACK);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_ONLY_TRACK.getName() + "/*/*", URI_KML_ONLY);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_WITH_TRACKDETAIL.getName() + "/*/*", URI_KML_WITH_TRACKDETAIL);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*/*", URI_KML_WITH_TRACKDETAIL_SENSORDATA);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_ONLY_TRACK.getName() + "/*/*", URI_KMZ_ONLY_TRACK);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL.getName() + "/*", URI_KMZ_WITH_TRACKDETAIL);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*", URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getName() + "/*", URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES);
}
public static Pair<Uri, String> createURI(long[] trackIds, @NonNull TrackFileFormat trackFileFormat) {
public static Pair<Uri, String> createURI(long[] trackIds, String trackName, @NonNull TrackFileFormat trackFileFormat) {
if (trackIds.length == 0) {
throw new UnsupportedOperationException();
}
StringBuilder builder = new StringBuilder();
StringBuilder trackIdBuilder = new StringBuilder();
for (long trackId : trackIds) {
builder.append(trackId).append(TRACKID_DELIMITER);
trackIdBuilder.append(trackId).append(TRACKID_DELIMITER);
}
builder.deleteCharAt(builder.lastIndexOf(TRACKID_DELIMITER));
trackIdBuilder.deleteCharAt(trackIdBuilder.lastIndexOf(TRACKID_DELIMITER));
Uri uri = Uri.parse(TracksColumns.CONTENT_URI + "/" + trackFileFormat.getName() + "/" + builder + "." + trackFileFormat.getExtension());
Uri uri = Uri.parse(TracksColumns.CONTENT_URI + "/" + trackFileFormat.getName() + "/" + trackIdBuilder + "/" + Uri.encode(trackName) + "." + trackFileFormat.getExtension());
String mime = getTypeMime(uri);
Log.d(TAG, "Created uri " + uri.toString() + " with MIME " + mime);
@@ -89,18 +90,18 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
return new Pair<>(uri, mime);
}
private static long[] parseURI(Uri uri) {
String lastPathSegment = uri.getLastPathSegment();
if (lastPathSegment == null) {
static long[] parseURI(Uri uri) {
List<String> uriPaths = uri.getPathSegments();
if (uriPaths == null || uriPaths.size() < 2) {
Log.d(TAG, "URI does not contain any trackIds.");
return new long[]{};
}
String fileExtension = "." + getTrackFileFormat(uri).getExtension();
String[] lastPathSegmentSplit = lastPathSegment.replace(fileExtension, "").split(TRACKID_DELIMITER);
String[] uriTrackIds = uriPaths.get(2).split(TRACKID_DELIMITER);
long[] trackIds = new long[lastPathSegmentSplit.length];
long[] trackIds = new long[uriTrackIds.length];
for (int i = 0; i < trackIds.length; i++) {
trackIds[i] = Long.valueOf(lastPathSegmentSplit[i]);
trackIds[i] = Long.parseLong(uriTrackIds[i]);
}
return trackIds;
}
@@ -150,7 +151,7 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
}
@Nullable
public static String getTypeMime(@NonNull Uri uri) {
private static String getTypeMime(@NonNull Uri uri) {
return getTrackFileFormat(uri).getMimeType();
}
@@ -0,0 +1,112 @@
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 CachedTrackPointsIndexes indexes;
private long lastTrackPointId = -1L;
private Cursor cursor;
public TrackPointIterator(ContentProviderUtils contentProviderUtils, long trackId, long startTrackPointId, boolean descending) {
this.contentProviderUtils = contentProviderUtils;
this.trackId = trackId;
this.descending = descending;
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);
return ContentProviderUtils.fillTrackPoint(cursor, indexes);
}
@Override
public void close() {
if (cursor != null) {
cursor.close();
cursor = null;
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
public int getCount() {
if (cursor == null) {
return -1;
}
return cursor.getCount();
}
}
@@ -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,7 +16,7 @@
package de.dennisguse.opentracks.fragments;
import android.location.Location;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -24,60 +24,104 @@ 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;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.chart.ChartView;
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;
import de.dennisguse.opentracks.stats.TripStatisticsUpdater;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* A fragment to display track chart to the user.
* ChartFragment uses a {@link TrackStatisticsUpdater} internally and recomputes the {@link TrackStatistics} from the beginning.
*
* @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";
private final List<double[]> pendingPoints = new ArrayList<>();
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<ChartPoint> pendingPoints = new ArrayList<>();
private TrackDataHub trackDataHub;
// Stats gathered from the received data
private TripStatisticsUpdater tripStatisticsUpdater;
private TrackStatisticsUpdater trackStatisticsUpdater;
private long startTime;
private boolean metricUnits = true;
private boolean reportSpeed = true;
//TODO Why is this needed?
private int recordingDistanceInterval;
// Modes of operation
private boolean chartByDistance;
private 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 (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
if (metricUnits != chartView.getMetricUnits()) {
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 reportSpeed = PreferencesUtils.isReportSpeed(getContext());
if (reportSpeed != chartView.getReportSpeed()) {
chartView.setReportSpeed(reportSpeed);
chartView.applyReportSpeed();
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 +137,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, true);
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 +167,9 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
public void onResume() {
super.onResume();
resumeTrackDataHub();
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
checkChartSettings();
getActivity().runOnUiThread(updateChart);
}
@@ -132,6 +178,7 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
public void onPause() {
super.onPause();
pauseTrackDataHub();
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
}
@Override
@@ -141,35 +188,21 @@ 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()) {
if (track == null || track.getTripStatistics() == null) {
if (track == null || track.getTrackStatistics() == null) {
startTime = -1L;
return;
}
startTime = track.getTripStatistics().getStartTime();
startTime = track.getTrackStatistics().getStartTime_ms();
}
}
@Override
public void clearTrackPoints() {
if (isResumed()) {
tripStatisticsUpdater = startTime != -1L ? new TripStatisticsUpdater(startTime) : null;
trackStatisticsUpdater = startTime != -1L ? new TrackStatisticsUpdater(startTime) : null;
pendingPoints.clear();
chartView.reset();
runOnUiThread(new Runnable() {
@@ -184,25 +217,25 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
}
@Override
public void onSampledInTrackPoint(Location location) {
public void onSampledInTrackPoint(TrackPoint trackPoint) {
if (isResumed()) {
double[] data = new double[ChartView.NUM_SERIES + 1];
fillDataPoint(location, data);
pendingPoints.add(data);
pendingPoints.add(createPendingPoint(trackPoint));
}
}
@Override
public void onSampledOutTrackPoint(Location location) {
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
if (isResumed()) {
fillDataPoint(location, null);
if (trackStatisticsUpdater != null) {
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
}
}
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
chartView.addDataPoints(pendingPoints);
chartView.addChartPoints(pendingPoints);
pendingPoints.clear();
runOnUiThread(updateChart);
}
@@ -229,112 +262,23 @@ 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.
*/
private void checkChartSettings() {
boolean needUpdate = false;
if (setSeriesEnabled(ChartView.SPEED_SERIES, reportSpeed)) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.PACE_SERIES, !reportSpeed)) {
needUpdate = true;
}
boolean needUpdate = chartView.applyReportSpeed();
if (needUpdate) {
chartView.postInvalidate();
}
}
/**
* Sets the series enabled value.
*
* @param index the series index
* @param value the value
* @return true if changed
*/
private boolean setSeriesEnabled(int index, boolean value) {
if (chartShow[index] != value) {
chartShow[index] = value;
chartView.setChartValueSeriesEnabled(index, value);
return true;
}
return false;
}
/**
* Resumes the trackDataHub.
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/
private synchronized void resumeTrackDataHub() {
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));
trackDataHub.registerTrackDataListener(this, true, true, true, true);
}
/**
@@ -354,16 +298,6 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
}
/**
* Reloads the trackDataHub.
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
*/
private synchronized void reloadTrackDataHub() {
if (trackDataHub != null) {
trackDataHub.reloadDataForListener(this);
}
}
/**
* Runs a runnable on the UI thread if possible.
*
@@ -376,82 +310,15 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
}
}
/**
* Given a location, fill in a data point, an array of double[]. <br>
* data[0] = time/distance <br>
* data[1] = elevation <br>
* data[2] = speed <br>
* data[3] = pace <br>
* data[4] = heart rate <br>
* data[5] = cadence <br>
* data[6] = power <br>
*
* @param location the location
* @param data the data point to fill in, can be null
*/
@VisibleForTesting
void fillDataPoint(Location location, double[] data) {
double timeOrDistance = Double.NaN;
double elevation = Double.NaN;
double speed = Double.NaN;
double pace = Double.NaN;
if (tripStatisticsUpdater != null) {
tripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
TripStatistics tripStatistics = tripStatisticsUpdater.getTripStatistics();
if (chartByDistance) {
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
timeOrDistance = distance;
} else {
timeOrDistance = tripStatistics.getTotalTime();
}
elevation = tripStatisticsUpdater.getSmoothedElevation();
if (!metricUnits) {
elevation *= UnitConversions.M_TO_FT;
}
speed = tripStatisticsUpdater.getSmoothedSpeed() * UnitConversions.MS_TO_KMH;
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
}
pace = speed == 0 ? 0.0 : 60.0 / speed;
}
double heartRate = Double.NaN;
double cadence = Double.NaN;
double power = Double.NaN;
if (location instanceof TrackPoint && ((TrackPoint) location).getSensorDataSet() != null) {
SensorDataSet sensorDataSet = ((TrackPoint) location).getSensorDataSet();
if (sensorDataSet.hasHeartRate()) {
heartRate = sensorDataSet.getHeartRate();
}
if (sensorDataSet.hasCadence()) {
cadence = sensorDataSet.getCadence();
}
if (sensorDataSet.hasPower()) {
power = sensorDataSet.getPower();
}
}
//TODO: Is related to ChartView.ELEVATION_SERIES etc.
if (data != null) {
data[0] = timeOrDistance;
data[1] = elevation;
data[2] = speed;
data[3] = pace;
data[4] = heartRate;
data[5] = cadence;
data[6] = power;
}
ChartPoint createPendingPoint(@NonNull TrackPoint trackPoint) {
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
return new ChartPoint(trackStatisticsUpdater, trackPoint.getSensorDataSet(), chartByDistance, chartView.getMetricUnits());
}
@VisibleForTesting
void setTripStatisticsUpdater(long time) {
tripStatisticsUpdater = new TripStatisticsUpdater(time);
void setTrackStatisticsUpdater(long time) {
trackStatisticsUpdater = new TrackStatisticsUpdater(time);
}
@VisibleForTesting
@@ -461,12 +328,12 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
@VisibleForTesting
void setMetricUnits(boolean value) {
metricUnits = value;
chartView.setMetricUnits(value);
}
@VisibleForTesting
void setReportSpeed(boolean value) {
reportSpeed = value;
chartView.setReportSpeed(value);
}
@VisibleForTesting
@@ -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);
@@ -32,7 +32,7 @@ import de.dennisguse.opentracks.util.ResourceUtils;
*
* @author apoorvn
*/
public class ChooseActivityTypeImageAdapter extends BaseAdapter {
class ChooseActivityTypeImageAdapter extends BaseAdapter {
private final List<Integer> imageIds;
private int selected = -1;
@@ -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();
}
@@ -40,18 +40,19 @@ import de.dennisguse.opentracks.io.file.TrackFileFormat;
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
@@ -67,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);
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,18 @@ 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 MenuItem shareMarkerImageMenuItem;
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);
@@ -143,6 +147,7 @@ public class MarkerDetailFragment extends Fragment {
// Need to update the waypoint in case returning after an edit
updateWaypoint(true);
updateUi();
updateMenuItems();
}
@Override
@@ -168,8 +173,14 @@ public class MarkerDetailFragment extends Fragment {
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.marker_detail, menu);
shareMarkerImageMenuItem = menu.findItem(R.id.marker_detail_share);
updateWaypoint(false);
updateMenuItems();
}
private void updateMenuItems() {
if (shareMarkerImageMenuItem != null)
shareMarkerImageMenuItem.setVisible(waypoint.hasPhoto());
}
@Override
@@ -186,6 +197,13 @@ public class MarkerDetailFragment extends Fragment {
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
startActivity(intent);
return true;
case R.id.marker_detail_share:
if (waypoint.hasPhoto()) {
intent = IntentUtils.newShareImageIntent(getContext(), waypoint.getPhotoURI());
intent = Intent.createChooser(intent, null);
startActivity(intent);
}
return true;
case R.id.marker_detail_delete:
DeleteMarkerDialogFragment.showDialog(getChildFragmentManager(), new long[]{markerId});
return true;
@@ -219,7 +237,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 +245,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,7 +16,7 @@
package de.dennisguse.opentracks.fragments;
import android.location.Location;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
@@ -33,20 +33,18 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import java.util.EnumSet;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity;
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.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.stats.TrackStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
@@ -61,9 +59,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;
@@ -71,12 +69,44 @@ public class StatsFragment extends Fragment implements TrackDataListener {
//TODO Initialize immediately and remove in onDestroy()
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private Location lastLocation = null;
private TripStatistics lastTripStatistics = null;
private TrackPoint lastTrackPoint = null;
private TrackStatistics lastTrackStatistics = 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());
if (PreferencesUtils.getRecordingTrackId(getContext()) != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
// A recording track id has been set -> Resumes track and starts timer.
resumeTrackDataHub();
if (trackRecordingServiceConnection == null) {
trackRecordingServiceConnection = new TrackRecordingServiceConnection(null);
}
trackRecordingServiceConnection.startConnection(getContext());
handlerUpdateUI.post(updateUIeachSecond);
}
}
}
};
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.stats, container, false);
@@ -91,7 +121,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
private final Runnable updateUIeachSecond = new Runnable() {
public void run() {
if (isResumed() && isSelectedTrackRecording()) {
if (!isSelectedTrackPaused() && lastTripStatistics != null) {
if (!isSelectedTrackPaused() && lastTrackStatistics != null) {
updateTotalTime();
updateSensorDataUI();
}
@@ -114,17 +144,12 @@ public class StatsFragment extends Fragment implements TrackDataListener {
private TextView speedMovingLabel;
private TextView speedMovingValue;
private TextView speedMovingUnit;
private View gradeElevationSeparator;
private View gradeElevationContainer;
private TextView elevationMinValue;
private TextView elevationMinUnit;
private TextView elevationMaxValue;
private TextView elevationMaxUnit;
private View elevationSeparator;
private View elevationContainer;
private View speedContainer;
private TextView speedLabel;
private TextView speedValue;
private TextView speedUnit;
private View elevationContainer;
private TextView elevationValue;
private TextView elevationUnit;
private View coordinateSeparator;
@@ -162,21 +187,14 @@ public class StatsFragment extends Fragment implements TrackDataListener {
speedMovingValue = view.findViewById(R.id.stats_moving_speed_value);
speedMovingUnit = view.findViewById(R.id.stats_moving_speed_unit);
gradeElevationSeparator = view.findViewById(R.id.stats_elevation_separator);
gradeElevationContainer = view.findViewById(R.id.stats_elevation_container);
elevationMinValue = view.findViewById(R.id.stats_elevation_min_value);
elevationMinUnit = view.findViewById(R.id.stats_elevation_min_unit);
elevationMaxValue = view.findViewById(R.id.stats_elevation_max_value);
elevationMaxUnit = view.findViewById(R.id.stats_elevation_max_unit);
elevationSeparator = view.findViewById(R.id.stats_elevation_separator);
elevationContainer = view.findViewById(R.id.stats_elevation_container);
speedContainer = view.findViewById(R.id.stats_speed);
speedLabel = view.findViewById(R.id.stats_speed_label);
speedValue = view.findViewById(R.id.stats_speed_value);
speedUnit = view.findViewById(R.id.stats_speed_unit);
elevationContainer = view.findViewById(R.id.stats_elevation);
elevationValue = view.findViewById(R.id.stats_elevation_current_value);
elevationUnit = view.findViewById(R.id.stats_elevation_current_unit);
@@ -221,8 +239,9 @@ public class StatsFragment extends Fragment implements TrackDataListener {
public void onResume() {
super.onResume();
resumeTrackDataHub();
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(null);
trackRecordingServiceConnection.startConnection(getContext());
handlerUpdateUI.post(updateUIeachSecond);
@@ -232,6 +251,8 @@ public class StatsFragment extends Fragment implements TrackDataListener {
public void onPause() {
super.onPause();
pauseTrackDataHub();
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
handlerUpdateUI.removeCallbacks(updateUIeachSecond);
}
@@ -273,21 +294,14 @@ public class StatsFragment extends Fragment implements TrackDataListener {
speedMovingValue = null;
speedMovingUnit = null;
gradeElevationSeparator = null;
gradeElevationContainer = null;
elevationMinValue = null;
elevationMinUnit = null;
elevationMaxValue = null;
elevationMaxUnit = null;
elevationSeparator = null;
elevationContainer = null;
speedContainer = null;
speedLabel = null;
speedValue = null;
speedUnit = null;
elevationContainer = null;
elevationValue = null;
elevationUnit = null;
@@ -305,7 +319,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void run() {
if (isResumed()) {
lastTripStatistics = track != null ? track.getTripStatistics() : null;
lastTrackStatistics = track != null ? track.getTrackStatistics() : null;
category = track != null ? track.getCategory() : "";
updateUI();
}
@@ -316,17 +330,17 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void clearTrackPoints() {
lastLocation = null;
lastTrackPoint = null;
}
@Override
public void onSampledInTrackPoint(Location location) {
lastLocation = location;
public void onSampledInTrackPoint(TrackPoint trackPoint) {
lastTrackPoint = trackPoint;
}
@Override
public void onSampledOutTrackPoint(Location location) {
lastLocation = location;
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
lastTrackPoint = trackPoint;
}
@Override
@@ -337,15 +351,15 @@ public class StatsFragment extends Fragment implements TrackDataListener {
public void run() {
if (isResumed()) {
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
lastLocation = null;
lastTrackPoint = null;
}
if (lastLocation != null) {
boolean hasFix = !LocationUtils.isLocationOld(lastLocation);
boolean hasGoodFix = lastLocation.hasAccuracy() && lastLocation.getAccuracy() < recordingGpsAccuracy;
if (lastTrackPoint != null) {
boolean hasFix = !LocationUtils.isLocationOld(lastTrackPoint.getLocation());
boolean hasGoodFix = lastTrackPoint.hasAccuracy() && lastTrackPoint.getAccuracy() < recordingGpsAccuracy;
if (!hasFix || !hasGoodFix) {
lastLocation = null;
lastTrackPoint = null;
}
}
setLocationValues();
@@ -370,57 +384,13 @@ 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.
*/
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));
trackDataHub.registerTrackDataListener(this, true, false, true, true);
}
/**
@@ -458,7 +428,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) {
@@ -503,7 +473,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
// Set total distance
{
double totalDistance = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getTotalDistance();
double totalDistance = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getTotalDistance();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
distanceValue.setText(parts.first);
@@ -522,14 +492,14 @@ public class StatsFragment extends Fragment implements TrackDataListener {
}
// Set time
if (lastTripStatistics != null) {
movingTimeValue.setText(StringUtils.formatElapsedTime(lastTripStatistics.getMovingTime()));
if (lastTrackStatistics != null) {
movingTimeValue.setText(StringUtils.formatElapsedTime(lastTrackStatistics.getMovingTime()));
updateTotalTime();
}
// Set average speed/pace
{
double speed = lastTripStatistics != null ? lastTripStatistics.getAverageSpeed() : Double.NaN;
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageSpeed() : Double.NaN;
speedAvgLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
@@ -539,7 +509,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
// Set max speed/pace
{
double speed = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getMaxSpeed();
double speed = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getMaxSpeed();
speedMaxLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
@@ -550,7 +520,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
// Set moving speed/pace
{
double speed = lastTripStatistics != null ? lastTripStatistics.getAverageMovingSpeed() : Double.NaN;
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageMovingSpeed() : Double.NaN;
speedMovingLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
@@ -559,44 +529,36 @@ public class StatsFragment extends Fragment implements TrackDataListener {
speedMovingUnit.setText(parts.second);
}
// Set elevation
// Make elevation visible?
{
boolean showElevation = PreferencesUtils.isShowStatsGradeElevation(getContext());
gradeElevationSeparator.setVisibility(showElevation ? View.VISIBLE : View.GONE);
gradeElevationContainer.setVisibility(showElevation ? View.VISIBLE : View.GONE);
if (showElevation) {
{
double elevation = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getMinElevation();
Pair<String, String> parts = StringUtils.formatElevation(getContext(), elevation, metricUnits);
elevationMinValue.setText(parts.first);
elevationMinUnit.setText(parts.second);
}
{
double elevation = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getMaxElevation();
Pair<String, String> parts = StringUtils.formatElevation(getContext(), elevation, metricUnits);
elevationMaxValue.setText(parts.first);
elevationMaxUnit.setText(parts.second);
}
}
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
elevationSeparator.setVisibility(showElevation ? View.VISIBLE : View.GONE);
}
}
private void updateTotalTime() {
long totalTime;
if (isSelectedTrackRecording()) {
totalTime = System.currentTimeMillis() - lastTripStatistics.getStopTime() + lastTripStatistics.getTotalTime();
totalTime = calculateTotalTime();
} else {
totalTime = lastTripStatistics.getTotalTime();
totalTime = lastTrackStatistics.getTotalTime();
}
totalTimeValueView.setText(StringUtils.formatElapsedTime(totalTime));
}
/**
* Return time from service.
* If service isn't bound then use lastTrackStatistics for calculate it.
*/
private long calculateTotalTime() {
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
return trackRecordingService.getTotalTime();
} else {
return System.currentTimeMillis() - lastTrackStatistics.getStopTime_ms() + lastTrackStatistics.getTotalTime();
}
}
private void setLocationValues() {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext());
@@ -607,18 +569,18 @@ public class StatsFragment extends Fragment implements TrackDataListener {
if (isRecording) {
speedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
double speed = lastLocation != null && lastLocation.hasSpeed() ? lastLocation.getSpeed() : Double.NaN;
double speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : Double.NaN;
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
speedValue.setText(parts.first);
speedUnit.setText(parts.second);
}
// Set elevation
boolean showGradeElevation = isRecording && PreferencesUtils.isShowStatsGradeElevation(getContext());
elevationContainer.setVisibility(showGradeElevation ? View.VISIBLE : View.GONE);
boolean showElevation = isRecording && PreferencesUtils.isShowStatsElevation(getContext());
elevationContainer.setVisibility(showElevation ? View.VISIBLE : View.GONE);
if (showGradeElevation) {
double altitude = lastLocation != null && lastLocation.hasAltitude() ? lastLocation.getAltitude() : Double.NaN;
if (showElevation) {
double altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? lastTrackPoint.getAltitude() : Double.NaN;
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, metricUnits);
elevationValue.setText(parts.first);
@@ -631,11 +593,11 @@ public class StatsFragment extends Fragment implements TrackDataListener {
coordinateSeparator.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
coordinateContainer.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
if (showCoordinate) {
double latitude = lastLocation != null ? lastLocation.getLatitude() : Double.NaN;
double latitude = lastTrackPoint != null ? lastTrackPoint.getLatitude() : Double.NaN;
String latitudeText = Double.isNaN(latitude) || Double.isInfinite(latitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(latitude);
latitudeValue.setText(latitudeText);
double longitude = lastLocation != null ? lastLocation.getLongitude() : Double.NaN;
double longitude = lastTrackPoint != null ? lastTrackPoint.getLongitude() : Double.NaN;
String longitudeText = Double.isNaN(longitude) || Double.isInfinite(longitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(longitude);
longitudeValue.setText(longitudeText);
}
@@ -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.
*/
@@ -28,11 +28,10 @@ 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.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
/**
@@ -41,7 +40,7 @@ import de.dennisguse.opentracks.util.SystemUtils;
* @author Jimmy Shih
*/
//TODO Make independent from ExportActivity?
public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ExportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat;
@@ -51,10 +50,10 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
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.
@@ -69,22 +68,6 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
this.directory = directory;
context = exportActivity.getApplicationContext();
contentProviderUtils = new ContentProviderUtils(context);
completed = false;
processedTrackCount = 0;
totalTrackCount = 0;
}
/**
* Sets the current activity associated with this AsyncTask.
*
* @param exportActivity the current activity, can be null
*/
public void setActivity(ExportActivity exportActivity) {
this.exportActivity = exportActivity;
if (completed && exportActivity != null) {
exportActivity.onAsyncTaskCompleted(processedTrackCount, totalTrackCount);
}
}
@Override
@@ -98,17 +81,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 +114,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,11 +129,15 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
};
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context, tracks, trackExporterListener);
Track track = tracks[0];
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context, new Track[]{track}, trackExporterListener);
String fileName = track.getId() + "." + trackFileFormat.getExtension();
DocumentFile file = directory.createFile(trackFileFormat.getMimeType(), fileName);
// 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)) {
@@ -183,7 +161,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 +173,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);
@@ -18,19 +18,17 @@ package de.dennisguse.opentracks.io.file.exporter;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
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.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.TrackPointIterator;
import de.dennisguse.opentracks.util.LocationUtils;
/**
@@ -72,9 +70,9 @@ public class FileTrackExporter implements TrackExporter {
writeWaypoints(track1);
}
trackWriter.writeBeginTracks();
long startTime = tracks[0].getTripStatistics().getStartTime();
long startTime = tracks[0].getTrackStatistics().getStartTime_ms();
for (Track track : tracks) {
long offset = track.getTripStatistics().getStartTime() - startTime;
long offset = track.getTrackStatistics().getStartTime_ms() - startTime;
writeLocations(track, offset);
}
trackWriter.writeEndTracks();
@@ -126,25 +124,25 @@ public class FileTrackExporter implements TrackExporter {
boolean wroteTrack = false;
boolean wroteSegment = false;
boolean isLastLocationValid = false;
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
int locationNumber = 0;
TrackPoint lastTrackPoint = null;
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, locationFactory)) {
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false)) {
while (locationIterator.hasNext()) {
while (trackPointIterator.hasNext()) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
Location location = locationIterator.next();
TrackPoint trackPoint = trackPointIterator.next();
setLocationTime(location, offset);
setLocationTime(trackPoint, offset);
locationNumber++;
boolean isLocationValid = LocationUtils.isValidLocation(location);
boolean isLocationValid = LocationUtils.isValidLocation(trackPoint.getLocation());
boolean isSegmentValid = isLocationValid && isLastLocationValid;
if (!wroteTrack && isSegmentValid) {
// Found the first two consecutive locations that are valid
trackWriter.writeBeginTrack(track, locationFactory.lastLocation);
trackWriter.writeBeginTrack(track, lastTrackPoint);
wroteTrack = true;
}
@@ -154,14 +152,14 @@ public class FileTrackExporter implements TrackExporter {
trackWriter.writeOpenSegment();
wroteSegment = true;
// Write the previous location, which we had previously skipped
trackWriter.writeLocation(locationFactory.lastLocation);
// Write the previous trackPoint, which we had previously skipped
trackWriter.writeTrackPoint(lastTrackPoint);
}
// Write the current location
trackWriter.writeLocation(location);
// Write the current trackPoint
trackWriter.writeTrackPoint(trackPoint);
if (trackExporterListener != null) {
trackExporterListener.onProgressUpdate(locationNumber, track.getNumberOfPoints());
trackExporterListener.onProgressUpdate(locationNumber, trackPointIterator.getCount());
}
} else {
if (wroteSegment) {
@@ -169,7 +167,7 @@ public class FileTrackExporter implements TrackExporter {
wroteSegment = false;
}
}
locationFactory.swapLocations();
lastTrackPoint = trackPoint;
isLastLocationValid = isLocationValid;
}
@@ -179,7 +177,7 @@ public class FileTrackExporter implements TrackExporter {
}
if (wroteTrack) {
Location lastValidTrackPoint = contentProviderUtils.getLastValidTrackPoint(track.getId());
TrackPoint lastValidTrackPoint = contentProviderUtils.getLastValidTrackPoint(track.getId());
setLocationTime(lastValidTrackPoint, offset);
trackWriter.writeEndTrack(track, lastValidTrackPoint);
} else {
@@ -191,41 +189,14 @@ public class FileTrackExporter implements TrackExporter {
}
/**
* Sets a location time.
* Sets a trackPoint time.
*
* @param location the location
* @param offset the time offset
* @param trackPoint the trackPoint
* @param offset the time offset
*/
private void setLocationTime(Location location, long offset) {
if (location != null) {
location.setTime(location.getTime() - offset);
}
}
/**
* Track writer location factory. Keeping the last two locations.
*
* @author Jimmy Shih
*/
private class TrackWriterLocationFactory extends LocationFactory {
Location currentLocation;
Location lastLocation;
@Override
public Location createLocation() {
if (currentLocation == null) {
currentLocation = new TrackPoint("");
}
return currentLocation;
}
void swapLocations() {
Location tempLocation = lastLocation;
lastLocation = currentLocation;
currentLocation = tempLocation;
if (currentLocation != null) {
currentLocation.reset();
}
private void setLocationTime(TrackPoint trackPoint, long offset) {
if (trackPoint != null) {
trackPoint.setTime(trackPoint.getTime() - offset);
}
}
}
@@ -24,6 +24,7 @@ import java.text.NumberFormat;
import java.util.Locale;
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.util.StringUtils;
@@ -139,7 +140,7 @@ public class GpxTrackWriter implements TrackWriter {
}
@Override
public void writeBeginTrack(Track track, Location startLocation) {
public void writeBeginTrack(Track track, TrackPoint startTrackPoint) {
if (printWriter != null) {
printWriter.println("<trk>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
@@ -150,7 +151,7 @@ public class GpxTrackWriter implements TrackWriter {
}
@Override
public void writeEndTrack(Track track, Location endLocation) {
public void writeEndTrack(Track track, TrackPoint endTrackPoint) {
if (printWriter != null) {
printWriter.println("</trk>");
}
@@ -167,14 +168,14 @@ public class GpxTrackWriter implements TrackWriter {
}
@Override
public void writeLocation(Location location) {
public void writeTrackPoint(TrackPoint trackPoint) {
if (printWriter != null) {
printWriter.println("<trkpt " + formatLocation(location) + ">");
if (location.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
printWriter.println("<trkpt " + formatLocation(trackPoint.getLocation()) + ">");
if (trackPoint.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(trackPoint.getAltitude()) + "</ele>");
}
printWriter.println(
"<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
"<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>");
printWriter.println("</trkpt>");
}
}
@@ -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.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,16 +65,16 @@ 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;
private TrackPoint startTrackPoint;
/**
* @param context the context
* @param hasMultipleTracks should encode multiple tracks into one file?
* @param exportTrackDetail should detailed information about the track be exported (e.g., title, description, waypoints, timing)?
* @param exportTrackDetail should detailed information about the track be exported (e.g., title, description, waypoints, timing)?
* @param exportSensorData should {@link SensorDataSet} be exported?
* @param exportPhotos should pictures be exported (if true: exports to KMZ)?
*/
@@ -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());
}
@@ -188,11 +188,12 @@ public class KmlTrackWriter implements TrackWriter {
}
@Override
public void writeBeginTrack(Track track, Location startLocation) {
this.startLocation = startLocation;
public void writeBeginTrack(Track track, TrackPoint startTrackPoint) {
this.startTrackPoint = startTrackPoint;
if (printWriter != null) {
String name = context.getString(R.string.marker_label_start, track.getName());
writePlacemark(name, "", "", START_STYLE, startLocation);
Location location = startTrackPoint != null ? startTrackPoint.getLocation() : null;
writePlacemark(name, "", "", START_STYLE, location);
printWriter.println("<Placemark>");
if (exportTrackDetail) {
@@ -210,7 +211,7 @@ public class KmlTrackWriter implements TrackWriter {
}
@Override
public void writeEndTrack(Track track, Location endLocation) {
public void writeEndTrack(Track track, TrackPoint endTrackPoint) {
if (printWriter != null) {
printWriter.println("</gx:MultiTrack>");
printWriter.println("</Placemark>");
@@ -218,7 +219,8 @@ public class KmlTrackWriter implements TrackWriter {
if (exportTrackDetail) {
String name = context.getString(R.string.marker_label_end, track.getName());
String description = descriptionGenerator.generateTrackDescription(track, false);
writePlacemark(name, "", description, END_STYLE, endLocation);
Location location = endTrackPoint != null ? endTrackPoint.getLocation() : null;
writePlacemark(name, "", description, END_STYLE, location);
}
}
}
@@ -256,16 +258,16 @@ public class KmlTrackWriter implements TrackWriter {
}
@Override
public void writeLocation(Location location) {
public void writeTrackPoint(TrackPoint trackPoint) {
if (printWriter != null) {
if (exportTrackDetail) {
printWriter.println("<when>" + getTime(location) + "</when>");
printWriter.println("<when>" + getTime(trackPoint.getLocation()) + "</when>");
}
printWriter.println("<gx:coord>" + getCoordinates(location, " ") + "</gx:coord>");
printWriter.println("<gx:coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</gx:coord>");
if (exportSensorData && location instanceof TrackPoint) {
SensorDataSet sensorDataSet = ((TrackPoint) location).getSensorDataSet();
if (exportSensorData) {
SensorDataSet sensorDataSet = trackPoint.getSensorDataSet();
if (sensorDataSet != null) {
if (sensorDataSet.hasHeartRate()) {
heartRateList.add(sensorDataSet.getHeartRate());
@@ -321,16 +323,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,11 +343,11 @@ 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) {
printWriter.println("<Icon><href>" + Uri.decode(photoUrl) + "</href></Icon>");
printWriter.println("<Icon><href>" + KmzTrackExporter.buildKmzImageFilePath(Uri.parse(photoUrl)) + "</href></Icon>");
}
printWriter.print("<ViewVolume>");
@@ -373,7 +373,7 @@ public class KmlTrackWriter implements TrackWriter {
if (exportTrackDetail) {
return StringUtils.formatDateTimeIso8601(location.getTime());
} else {
return StringUtils.formatDateTimeIso8601(location.getTime() - startLocation.getTime());
return StringUtils.formatDateTimeIso8601(location.getTime() - startTrackPoint.getTime());
}
}
@@ -388,7 +388,7 @@ public class KmlTrackWriter implements TrackWriter {
if (trackPointId == -1L) {
return location.getBearing();
}
Location viewLocation;
TrackPoint viewLocation;
try (Cursor cursor = contentProviderUtils.getTrackPointCursor(trackId, trackPointId, 10, true)) {
if (cursor == null || cursor.getCount() == 0) {
return location.getBearing();
@@ -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.
@@ -126,7 +126,7 @@ public class KmzTrackExporter implements TrackExporter {
Uri uri = Uri.parse(photoUrl);
try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) {
ZipEntry zipEntry = new ZipEntry(KMZ_IMAGES_DIR + File.separatorChar + FileUtils.sanitizeFileName(uri.getLastPathSegment()));
ZipEntry zipEntry = new ZipEntry(buildKmzImageFilePath(uri));
zipOutputStream.putNextEntry(zipEntry);
if (inputStream == null) throw new FileNotFoundException();
@@ -147,4 +147,13 @@ public class KmzTrackExporter implements TrackExporter {
outputStream.write(buffer, 0, byteCount);
}
}
/**
* Builds and returns the path for the file that will be saved inside KMZ_IMAGES_DIR.
*
* @param uri URI object.
*/
public static String buildKmzImageFilePath(Uri uri) {
return KMZ_IMAGES_DIR + File.separatorChar + FileUtils.sanitizeFileName(uri.getLastPathSegment());
}
}
@@ -25,8 +25,8 @@ public interface TrackExporterListener {
/**
* Called to update progress.
*
* @param number the number of locations written
* @param max the maximum number of locations in a track, for calculation of
* @param number the number of {@link de.dennisguse.opentracks.content.data.TrackPoint}s written
* @param max the maximum number of {@link de.dennisguse.opentracks.content.data.TrackPoint}s in a {@link de.dennisguse.opentracks.content.data.Track}, for calculation of
* completion percentage
*/
void onProgressUpdate(int number, int max);
@@ -15,11 +15,10 @@
*/
package de.dennisguse.opentracks.io.file.exporter;
import android.location.Location;
import java.io.OutputStream;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
/**
@@ -35,13 +34,13 @@ import de.dennisguse.opentracks.content.data.Waypoint;
* {@link #writeEndWaypoints()}
* {@link #writeBeginTracks()}
* For each track:
* {@link #writeBeginTrack(Track, Location)}
* {@link #writeBeginTrack(Track, TrackPoint)}
* For each segment:
* {@link #writeOpenSegment()}
* For each location in the segment:
* {@link #writeLocation(Location)}
* For each trackPoint in the segment:
* {@link #writeTrackPoint(TrackPoint)}
* {@link #writeCloseSegment()}
* {@link #writeEndTrack(Track, Location)}
* {@link #writeEndTrack(Track, TrackPoint)}
* {@link #writeEndTracks()}
* {@link #writeFooter()}
* {@link #close()}
@@ -107,18 +106,18 @@ public interface TrackWriter {
/**
* Writes the beginning of a track.
*
* @param track the track
* @param startLocation the start location
* @param track the track
* @param startTrackPoint the start location
*/
void writeBeginTrack(Track track, Location startLocation);
void writeBeginTrack(Track track, TrackPoint startTrackPoint);
/**
* Writes the end of a track.
*
* @param track the track
* @param endLocation the end location
* @param track the track
* @param endTrackPoint the end location
*/
void writeEndTrack(Track track, Location endLocation);
void writeEndTrack(Track track, TrackPoint endTrackPoint);
/**
* Writes open segment.
@@ -131,9 +130,9 @@ public interface TrackWriter {
void writeCloseSegment();
/**
* Writes a location.
* Writes a trackPoint.
*
* @param location the location
* @param trackPoint the trackPoint
*/
void writeLocation(Location location);
void writeTrackPoint(TrackPoint trackPoint);
}
@@ -17,8 +17,6 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.util.Log;
@@ -38,13 +36,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.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TripStatisticsUpdater;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -158,11 +155,12 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
int waypointPosition = -1;
Waypoint waypoint = null;
Location location = null;
TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
TrackPoint trackPoint = null;
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics().getStartTime_ms());
// TODO Should not be necessary anymore?
TrackStatisticsUpdater markerTrackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics().getStartTime_ms());
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false)) {
while (true) {
if (waypoint == null) {
@@ -174,38 +172,38 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
}
}
if (location == null) {
if (!locationIterator.hasNext()) {
if (trackPoint == null) {
if (!trackPointIterator.hasNext()) {
// No more track points. Ignore the rest of the waypoints.
return;
}
location = locationIterator.next();
trackTripStatisticstrackUpdater.addLocation(location, recordingDistanceInterval);
markerTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
trackPoint = trackPointIterator.next();
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
markerTrackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
}
if (waypoint.getLocation().getTime() > location.getTime()) {
location = null;
} else if (waypoint.getLocation().getTime() < location.getTime()) {
Log.w(TAG, "Ignoring waypoint: current location was after waypoint.");
if (waypoint.getLocation().getTime() > trackPoint.getTime()) {
trackPoint = null;
} else if (waypoint.getLocation().getTime() < trackPoint.getTime()) {
Log.w(TAG, "Ignoring waypoint: current trackPoint was after waypoint.");
waypoint = null;
} else {
// The waypoint location time matches the track point time
if (!LocationUtils.isValidLocation(location)) {
// Invalid location, load the next location
location = null;
// The waypoint trackPoint time matches the track point time
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
// Invalid trackPoint, load the next trackPoint
trackPoint = null;
continue;
}
// Valid location
if (location.getLatitude() == waypoint.getLocation().getLatitude() && location.getLongitude() == waypoint.getLocation().getLongitude()) {
// Valid trackPoint
if (trackPoint.getLatitude() == waypoint.getLocation().getLatitude() && trackPoint.getLongitude() == waypoint.getLocation().getLongitude()) {
String waypointDescription = waypoint.getDescription();
String icon = context.getString(R.string.marker_waypoint_icon_url);
double length = trackTripStatisticstrackUpdater.getTripStatistics().getTotalDistance();
long duration = trackTripStatisticstrackUpdater.getTripStatistics().getTotalTime();
double length = trackStatisticsUpdater.getTrackStatistics().getTotalDistance();
long duration = trackStatisticsUpdater.getTrackStatistics().getTotalTime();
// Insert waypoint
Waypoint newWaypoint = new Waypoint(waypoint.getName(), waypointDescription, waypoint.getCategory(), icon, track.getId(), length, duration, location, waypoint.getPhotoUrl());
Waypoint newWaypoint = new Waypoint(waypoint.getName(), waypointDescription, waypoint.getCategory(), icon, track.getId(), length, duration, trackPoint.getLocation(), waypoint.getPhotoUrl());
contentProviderUtils.insertWaypoint(newWaypoint);
}
@@ -256,12 +254,11 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (icon != null) {
trackData.track.setIcon(icon);
}
if (trackData.tripStatisticsUpdater == null) {
trackData.tripStatisticsUpdater = new TripStatisticsUpdater(trackData.importTime);
trackData.tripStatisticsUpdater.updateTime(trackData.importTime);
if (trackData.trackStatisticsUpdater == null) {
trackData.trackStatisticsUpdater = new TrackStatisticsUpdater(trackData.importTime);
trackData.trackStatisticsUpdater.updateTime(trackData.importTime);
}
trackData.track.setTripStatistics(trackData.tripStatisticsUpdater.getTripStatistics());
trackData.track.setNumberOfPoints(trackData.numberOfLocations);
trackData.track.setTrackStatistics(trackData.trackStatisticsUpdater.getTrackStatistics());
contentProviderUtils.updateTrack(trackData.track);
}
@@ -273,7 +270,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(TrackPoint.createPauseWithTime(trackData.lastLocationInCurrentSegment.getTime()));
}
trackData.lastLocationInCurrentSegment = null;
}
@@ -287,13 +284,12 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
return;
}
Waypoint waypoint = new Waypoint();
Location location = createLocation();
TrackPoint trackPoint = createTrackPoint();
if (!LocationUtils.isValidLocation(location)) {
throw new SAXException(createErrorMessage("Invalid location detected: " + location));
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
}
waypoint.setLocation(location);
Waypoint waypoint = new Waypoint(trackPoint.getLocation());
if (name != null) {
waypoint.setName(name);
@@ -314,12 +310,12 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
/**
* Gets a track point.
*/
protected Location getTrackPoint() throws SAXException {
Location location = createLocation();
protected TrackPoint getTrackPoint() throws SAXException {
TrackPoint trackPoint = createTrackPoint();
// Calculate derived attributes from the previous point
if (trackData.lastLocationInCurrentSegment != null && trackData.lastLocationInCurrentSegment.getTime() != 0) {
long timeDifference = location.getTime() - trackData.lastLocationInCurrentSegment.getTime();
long timeDifference = trackPoint.getTime() - trackData.lastLocationInCurrentSegment.getTime();
// Check for negative time change
if (timeDifference <= 0) {
@@ -331,32 +327,31 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* GPS points tend to have some inherent imprecision, speed and bearing will likely be off, so the statistics for things like max speed will also be off.
*/
double duration = timeDifference * UnitConversions.MS_TO_S;
double speed = trackData.lastLocationInCurrentSegment.distanceTo(location) / duration;
location.setSpeed((float) speed);
double speed = trackData.lastLocationInCurrentSegment.distanceTo(trackPoint) / duration;
trackPoint.setSpeed((float) speed);
}
location.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(location));
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
}
if (!LocationUtils.isValidLocation(location)) {
throw new SAXException(createErrorMessage("Invalid location detected: " + location));
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
}
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(TrackPoint.createResumeWithTime(trackPoint.getTime()));
}
trackData.lastLocationInCurrentSegment = location;
return location;
trackData.lastLocationInCurrentSegment = trackPoint;
return trackPoint;
}
/**
* Inserts a track point.
*
* @param location the location
* @param trackPoint the trackPoint
*/
protected void insertTrackPoint(Location location) {
insertLocation(location);
protected void insertTrackPoint(TrackPoint trackPoint) {
insertLocation(trackPoint);
}
/**
@@ -371,21 +366,42 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
/**
* Gets the photo url for a file.
*
* @param fileName the file name
* @param externalPhotoUrl the file name
*/
protected String getPhotoUrl(String fileName) {
protected String getInternalPhotoUrl(String externalPhotoUrl) {
if (importTrackId == -1L) {
Log.e(TAG, "Track id is invalid.");
return null;
}
if (externalPhotoUrl == null) {
Log.i(TAG, "External photo url is null.");
return null;
}
Uri externalPhotoUri = Uri.parse(externalPhotoUrl);
if (externalPhotoUri == null) {
Log.w(TAG, "Could not parse external photo url.");
return null;
}
String filename = externalPhotoUri.getLastPathSegment();
if (filename == null) {
Log.w(TAG, "External photo contains no filename.");
return null;
}
File dir = FileUtils.getPhotoDir(context, importTrackId);
File file = new File(dir, fileName);
return Uri.fromFile(file).toString();
File file = new File(dir, filename);
Uri photoUri = FileUtils.getUriForFile(context, file);
return "" + photoUri;
}
/**
* Creates a location.
*/
private Location createLocation() throws SAXException {
private TrackPoint createTrackPoint() throws SAXException {
if (latitude == null || longitude == null) {
return null;
}
@@ -411,50 +427,26 @@ 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);
}
}
return createLocation(latitudeValue, longitudeValue, altitudeValue, timeValue);
return new TrackPoint(latitudeValue, longitudeValue, altitudeValue, timeValue);
}
/**
* Creates a location.
* Inserts a trackPoint.
*
* @param latitudeValue the latitude value
* @param longitudeValue the longitude value
* @param altitudeValue the altitude value
* @param timeValue the time value
* @param trackPoint the trackPoint
*/
private Location createLocation(double latitudeValue, double longitudeValue, Double altitudeValue, long timeValue) {
Location location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(latitudeValue);
location.setLongitude(longitudeValue);
if (altitudeValue != null) {
location.setAltitude(altitudeValue);
} else {
location.removeAltitude();
private void insertLocation(TrackPoint trackPoint) {
if (trackData.trackStatisticsUpdater == null) {
trackData.trackStatisticsUpdater = new TrackStatisticsUpdater(trackPoint.getTime() != -1L ? trackPoint.getTime() : trackData.importTime);
}
location.setTime(timeValue);
location.removeAccuracy();
location.removeBearing();
location.removeSpeed();
return location;
}
trackData.trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
/**
* Inserts a location.
*
* @param location the location
*/
private void insertLocation(Location location) {
if (trackData.tripStatisticsUpdater == null) {
trackData.tripStatisticsUpdater = new TripStatisticsUpdater(location.getTime() != -1L ? location.getTime() : trackData.importTime);
}
trackData.tripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
trackData.bufferedLocations[trackData.numBufferedLocations] = location;
trackData.bufferedTrackPoints[trackData.numBufferedLocations] = trackPoint;
trackData.numBufferedLocations++;
trackData.numberOfLocations++;
@@ -472,7 +464,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (data.numBufferedLocations <= 0) {
return;
}
contentProviderUtils.bulkInsertTrackPoint(data.bufferedLocations, data.numBufferedLocations, data.track.getId());
contentProviderUtils.bulkInsertTrackPoint(data.bufferedTrackPoints, data.numBufferedLocations, data.track.getId());
data.numBufferedLocations = 0;
}
@@ -491,27 +483,27 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* @author Jimmy Shih
*/
// TODO Why private inner class?
private class TrackData {
private static 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;
// The last location in the current segment; Null if the current segment doesn't have a last location
Location lastLocationInCurrentSegment;
TrackPoint lastLocationInCurrentSegment;
// The number of locations processed for the current track
int numberOfLocations = 0;
// The trip statistics updater for the current track
TripStatisticsUpdater tripStatisticsUpdater;
// The TrackStatisticsUpdater for the current track
TrackStatisticsUpdater trackStatisticsUpdater;
// 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 TrackPoint[] bufferedTrackPoints = new TrackPoint[MAX_BUFFERED_LOCATIONS];
// The number of buffered locations
int numBufferedLocations = 0;

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