forked from upstream-mirrors/OpenTracks
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 928781391d | |||
| 05cd672464 | |||
| d3109bbc37 | |||
| eabdb7c721 | |||
| ad990e8cd7 | |||
| db3eef0104 | |||
| c4f1ba7962 | |||
| 7451fbd413 | |||
| b6c55de55a | |||
| dd3ee6d99e | |||
| bbb6a7c22c | |||
| dc1b50a7bf | |||
| 21bfb88e0b | |||
| aaab8adce9 | |||
| df502f835c | |||
| dd6a4cd153 | |||
| 76656997ef | |||
| 32070a0bd6 | |||
| b9420d4dde | |||
| 1b810e1294 | |||
| f16d59bff6 | |||
| b5bf3aadc5 | |||
| f6a27a1522 | |||
| 83758fcd72 | |||
| 070917bc0b | |||
| 543a04dfdd | |||
| b4d5eb0eec | |||
| 9f84cc4952 | |||
| 09fe35f1ca | |||
| 27e50b2dd9 | |||
| 971460848b | |||
| 8bc2937bb4 | |||
| 0e9b68c2a1 | |||
| ba298c2afb | |||
| 79f5ed4dc3 |
@@ -0,0 +1,21 @@
|
||||
#https://github.com/marketplace/actions/android-emulator-runner
|
||||
name: Test
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
api-level: [21, 29]
|
||||
target: [default]
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: run tests
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
target: ${{ matrix.target }}
|
||||
arch: x86_64
|
||||
script: ./gradlew connectedCheck
|
||||
+10
-10
@@ -22,18 +22,18 @@ android:
|
||||
- android-29
|
||||
|
||||
# Emulator Management: Create, Start and Wait
|
||||
#install:
|
||||
# - echo y | sdkmanager "platform-tools"
|
||||
# - echo y | sdkmanager --channel=4 "emulator"
|
||||
# - echo y | sdkmanager "system-images;android-$API;$EMU_FLAVOR;$ABI"
|
||||
# - echo no | avdmanager create avd --force -n test -k "system-images;android-$API;$EMU_FLAVOR;$ABI" -c 10M
|
||||
# - emulator -verbose -avd test -no-accel -no-snapshot -no-window -camera-back none -camera-front none -selinux permissive -qemu -m 2048 &
|
||||
# - android-wait-for-emulator
|
||||
# - adb shell input keyevent 82 &
|
||||
install:
|
||||
- echo y | sdkmanager "platform-tools"
|
||||
- echo y | sdkmanager --channel=4 "emulator"
|
||||
- echo y | sdkmanager "system-images;android-$API;$EMU_FLAVOR;$ABI"
|
||||
- echo no | avdmanager create avd --force -n test -k "system-images;android-$API;$EMU_FLAVOR;$ABI" -c 10M
|
||||
- emulator -verbose -avd test -no-accel -no-snapshot -no-window -camera-back none -camera-front none -selinux permissive -qemu -m 2048 &
|
||||
- android-wait-for-emulator
|
||||
- adb shell input keyevent 82 &
|
||||
|
||||
script:
|
||||
- ./gradlew clean build
|
||||
# - ./gradlew connectedCheck
|
||||
- ./gradlew build
|
||||
- ./gradlew connectedCheck
|
||||
|
||||
sbefore_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
|
||||
+3
-3
@@ -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`.
|
||||
|
||||
+2
-1
@@ -98,6 +98,7 @@ android {
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
@@ -109,5 +110,5 @@ dependencies {
|
||||
|
||||
testImplementation 'androidx.test:core:1.2.0'
|
||||
testImplementation 'org.mockito:mockito-core:3.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -38,6 +38,7 @@ import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
|
||||
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.stats.TripStatistics;
|
||||
|
||||
/**
|
||||
|
||||
+12
-7
@@ -14,8 +14,9 @@
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.content;
|
||||
package de.dennisguse.opentracks.content.provider;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
@@ -26,7 +27,6 @@ import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import de.dennisguse.opentracks.content.CustomContentProvider.DatabaseHelper;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.data.WaypointsColumns;
|
||||
@@ -47,13 +47,13 @@ public class CustomContentProviderTest {
|
||||
@Before
|
||||
public void setUp() {
|
||||
context.deleteDatabase(DATABASE_NAME);
|
||||
db = (new DatabaseHelper(context, DATABASE_NAME)).getWritableDatabase();
|
||||
db = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getWritableDatabase();
|
||||
customContentProvider = new CustomContentProvider() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link CustomContentProvider.DatabaseHelper#onCreate(SQLiteDatabase)}.
|
||||
* Tests {@link CustomSQLiteOpenHelper#onCreate(SQLiteDatabase)}.
|
||||
*/
|
||||
@Test
|
||||
public void testDatabaseHelper_OnCreate() {
|
||||
@@ -76,8 +76,13 @@ public class CustomContentProviderTest {
|
||||
@Test
|
||||
public void testGetType() {
|
||||
Assert.assertEquals(TracksColumns.CONTENT_TYPE, customContentProvider.getType(TracksColumns.CONTENT_URI));
|
||||
Assert.assertEquals(TrackPointsColumns.CONTENT_TYPE, customContentProvider.getType(TrackPointsColumns.CONTENT_URI));
|
||||
Assert.assertEquals(TracksColumns.CONTENT_ITEMTYPE, customContentProvider.getType(ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), 1).build()));
|
||||
|
||||
Assert.assertEquals(TrackPointsColumns.CONTENT_TYPE, customContentProvider.getType(TrackPointsColumns.CONTENT_URI_BY_ID));
|
||||
Assert.assertEquals(TrackPointsColumns.CONTENT_ITEMTYPE, customContentProvider.getType(ContentUris.appendId(TrackPointsColumns.CONTENT_URI_BY_TRACKID.buildUpon(), 1).build()));
|
||||
|
||||
Assert.assertEquals(WaypointsColumns.CONTENT_TYPE, customContentProvider.getType(WaypointsColumns.CONTENT_URI));
|
||||
Assert.assertEquals(WaypointsColumns.CONTENT_ITEMTYPE, customContentProvider.getType(ContentUris.appendId(WaypointsColumns.CONTENT_URI.buildUpon(), 1).build()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,7 +147,7 @@ public class CustomContentProviderTest {
|
||||
createTable(TrackPointsColumns.TABLE_NAME);
|
||||
createTable(WaypointsColumns.TABLE_NAME);
|
||||
|
||||
DatabaseHelper databaseHelper = new DatabaseHelper(context);
|
||||
databaseHelper.onUpgrade(db, oldVersion, CustomContentProvider.DATABASE_VERSION);
|
||||
CustomSQLiteOpenHelper databaseHelper = new CustomSQLiteOpenHelper(context);
|
||||
databaseHelper.onUpgrade(db, oldVersion, CustomSQLiteOpenHelper.DATABASE_VERSION);
|
||||
}
|
||||
}
|
||||
+42
-59
@@ -13,7 +13,7 @@
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package de.dennisguse.opentracks.content;
|
||||
package de.dennisguse.opentracks.content.provider;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
@@ -37,6 +37,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
@@ -75,16 +76,16 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testLocationIterator_noPoints() {
|
||||
testIterator(1, 0, 1, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
|
||||
testIterator(1, 0, 1, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLocationIterator_customFactory() {
|
||||
final Location location = new Location("test_location");
|
||||
final TrackPoint location = new TrackPoint("test_location");
|
||||
final AtomicInteger counter = new AtomicInteger();
|
||||
testIterator(1, 15, 4, false, new LocationFactory() {
|
||||
testIterator(1, 15, 4, false, new TrackPointFactory() {
|
||||
@Override
|
||||
public Location createLocation() {
|
||||
public TrackPoint createLocation() {
|
||||
counter.incrementAndGet();
|
||||
return location;
|
||||
}
|
||||
@@ -105,45 +106,45 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
@Test
|
||||
public void testLocationIterator_noBatchAscending() {
|
||||
testIterator(1, 50, 100, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
|
||||
testIterator(2, 50, 50, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
|
||||
testIterator(1, 50, 100, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
|
||||
testIterator(2, 50, 50, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLocationIterator_noBatchDescending() {
|
||||
testIterator(1, 50, 100, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
|
||||
testIterator(2, 50, 50, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
|
||||
testIterator(1, 50, 100, true, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
|
||||
testIterator(2, 50, 50, true, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLocationIterator_batchAscending() {
|
||||
testIterator(1, 50, 11, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
|
||||
testIterator(2, 50, 25, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
|
||||
testIterator(1, 50, 11, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
|
||||
testIterator(2, 50, 25, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLocationIterator_batchDescending() {
|
||||
testIterator(1, 50, 11, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
|
||||
testIterator(2, 50, 25, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
|
||||
testIterator(1, 50, 11, true, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
|
||||
testIterator(2, 50, 25, true, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLocationIterator_largeTrack() {
|
||||
testIterator(1, 20000, 2000, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
|
||||
testIterator(1, 20000, 2000, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
|
||||
}
|
||||
|
||||
private List<Location> testIterator(long trackId, int numPoints, int batchSize, boolean descending, LocationFactory locationFactory) {
|
||||
private List<Location> testIterator(long trackId, int numPoints, int batchSize, boolean descending, TrackPointFactory trackPointFactory) {
|
||||
long lastPointId = initializeTrack(trackId, numPoints);
|
||||
((ContentProviderUtils) contentProviderUtils).setDefaultCursorBatchSize(batchSize);
|
||||
List<Location> locations = new ArrayList<Location>(numPoints);
|
||||
try (LocationIterator it = contentProviderUtils.getTrackPointLocationIterator(trackId, -1L, descending, locationFactory)) {
|
||||
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(trackId, -1L, descending, trackPointFactory)) {
|
||||
while (it.hasNext()) {
|
||||
Location loc = it.next();
|
||||
Assert.assertNotNull(loc);
|
||||
locations.add(loc);
|
||||
// Make sure the IDs are returned in the right order.
|
||||
Assert.assertEquals(descending ? lastPointId - locations.size() + 1
|
||||
: lastPointId - numPoints + locations.size(), it.getLocationId());
|
||||
: lastPointId - numPoints + locations.size(), it.getTrackPointId());
|
||||
}
|
||||
Assert.assertEquals(numPoints, locations.size());
|
||||
}
|
||||
@@ -173,10 +174,10 @@ public class CustomContentProviderUtilsTest {
|
||||
// Load all inserted locations.
|
||||
long lastPointId = -1;
|
||||
int counter = 0;
|
||||
try (LocationIterator it = contentProviderUtils.getTrackPointLocationIterator(id, -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
|
||||
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(id, -1L, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY)) {
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
lastPointId = it.getLocationId();
|
||||
lastPointId = it.getTrackPointId();
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
@@ -229,7 +230,7 @@ public class CustomContentProviderUtilsTest {
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
|
||||
Assert.assertEquals(1, tracksCursor.getCount());
|
||||
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, null, TrackPointsColumns._ID);
|
||||
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
|
||||
Assert.assertEquals(10, tracksPointsCursor.getCount());
|
||||
Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
|
||||
Assert.assertEquals(1, waypointCursor.getCount());
|
||||
@@ -238,7 +239,7 @@ public class CustomContentProviderUtilsTest {
|
||||
// Check whether all have been deleted.
|
||||
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
|
||||
Assert.assertEquals(0, tracksCursor.getCount());
|
||||
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, null, TrackPointsColumns._ID);
|
||||
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
|
||||
Assert.assertEquals(0, tracksPointsCursor.getCount());
|
||||
waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
|
||||
Assert.assertEquals(0, waypointCursor.getCount());
|
||||
@@ -264,7 +265,7 @@ public class CustomContentProviderUtilsTest {
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
|
||||
Assert.assertEquals(3, tracksCursor.getCount());
|
||||
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, null, TrackPointsColumns._ID);
|
||||
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
|
||||
Assert.assertEquals(20, tracksPointsCursor.getCount());
|
||||
Cursor waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
|
||||
Assert.assertEquals(1, waypointCursor.getCount());
|
||||
@@ -273,7 +274,7 @@ public class CustomContentProviderUtilsTest {
|
||||
// Check whether all data of a track has been deleted.
|
||||
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
|
||||
Assert.assertEquals(2, tracksCursor.getCount());
|
||||
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI, null, null, null, TrackPointsColumns._ID);
|
||||
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
|
||||
Assert.assertEquals(20, tracksPointsCursor.getCount());
|
||||
waypointCursor = contentResolver.query(WaypointsColumns.CONTENT_URI, null, null, null, WaypointsColumns._ID);
|
||||
Assert.assertEquals(0, waypointCursor.getCount());
|
||||
@@ -414,7 +415,7 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
/**
|
||||
* Tests the method
|
||||
* {@link ContentProviderUtils#deleteWaypoint(Context, long)}
|
||||
* {@link ContentProviderUtils#deleteWaypoint(long)}
|
||||
* when there is only one waypoint in the track.
|
||||
*/
|
||||
@Test
|
||||
@@ -430,14 +431,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
|
||||
@@ -475,7 +476,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 +507,6 @@ public class CustomContentProviderUtilsTest {
|
||||
Assert.assertEquals(4, contentProviderUtils.getNextWaypointNumber(trackId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the method
|
||||
* {@link ContentProviderUtils#getLastWaypoint(long)}.
|
||||
*/
|
||||
@Test
|
||||
public void testGetLastWaypoint() {
|
||||
long trackId = System.currentTimeMillis();
|
||||
Track track = TestDataUtil.getTrack(trackId, 10);
|
||||
contentProviderUtils.insertTrack(track);
|
||||
|
||||
Waypoint waypoint1 = new Waypoint();
|
||||
waypoint1.setTrackId(trackId);
|
||||
waypoint1.setDescription("Desc1");
|
||||
Waypoint waypoint2 = new Waypoint();
|
||||
waypoint2.setTrackId(trackId);
|
||||
waypoint2.setDescription("Desc2");
|
||||
Waypoint waypoint3 = new Waypoint();
|
||||
waypoint3.setTrackId(trackId);
|
||||
waypoint3.setDescription("Desc3");
|
||||
contentProviderUtils.insertWaypoint(waypoint1);
|
||||
contentProviderUtils.insertWaypoint(waypoint2);
|
||||
contentProviderUtils.insertWaypoint(waypoint3);
|
||||
|
||||
Assert.assertEquals("Desc3", contentProviderUtils.getLastWaypoint(trackId).getDescription());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the method {@link ContentProviderUtils#insertWaypoint(Waypoint)} and
|
||||
* {@link ContentProviderUtils#getWaypoint(long)}.
|
||||
@@ -715,7 +690,7 @@ public class CustomContentProviderUtilsTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in descending.
|
||||
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, TrackPointFactory)} in descending.
|
||||
*/
|
||||
@Test
|
||||
public void testGetTrackPointLocationIterator_desc() {
|
||||
@@ -731,18 +706,18 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
long startTrackPointId = trackpointIds[9];
|
||||
|
||||
LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
|
||||
TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
|
||||
for (int i = 0; i < trackpointIds.length; i++) {
|
||||
Assert.assertTrue(locationIterator.hasNext());
|
||||
Location location = locationIterator.next();
|
||||
Assert.assertEquals(startTrackPointId - i, locationIterator.getLocationId());
|
||||
Assert.assertEquals(startTrackPointId - i, locationIterator.getTrackPointId());
|
||||
checkLocation((trackpointIds.length - 1) - i, location);
|
||||
}
|
||||
Assert.assertFalse(locationIterator.hasNext());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in ascending.
|
||||
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, TrackPointFactory)} in ascending.
|
||||
*/
|
||||
@Test
|
||||
public void testGetTrackPointLocationIterator_asc() {
|
||||
@@ -758,11 +733,11 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
long startTrackPointId = trackpointIds[0];
|
||||
|
||||
LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
|
||||
TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY);
|
||||
for (int i = 0; i < trackpointIds.length; i++) {
|
||||
Assert.assertTrue(locationIterator.hasNext());
|
||||
Location location = locationIterator.next();
|
||||
Assert.assertEquals(startTrackPointId + i, locationIterator.getLocationId());
|
||||
Assert.assertEquals(startTrackPointId + i, locationIterator.getTrackPointId());
|
||||
|
||||
checkLocation(i, location);
|
||||
}
|
||||
@@ -791,4 +766,12 @@ public class CustomContentProviderUtilsTest {
|
||||
contentProviderUtils.insertTrack(track);
|
||||
contentProviderUtils.bulkInsertTrackPoint(track.getLocations().toArray(new Location[0]), track.getLocations().size(), track.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatIdListForUri() {
|
||||
Assert.assertEquals("", ContentProviderUtils.formatIdListForUri(new long[]{}));
|
||||
Assert.assertEquals("12", ContentProviderUtils.formatIdListForUri(new long[]{12}));
|
||||
Assert.assertEquals("42,43,44", ContentProviderUtils.formatIdListForUri(new long[]{42, 43, 44}));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -61,8 +61,7 @@ public class ChartFragmentTest {
|
||||
@Before
|
||||
public void setUp() {
|
||||
boolean chartByDistance = false;
|
||||
chartFragment = new ChartFragment(chartByDistance) {
|
||||
};
|
||||
chartFragment = (ChartFragment) ChartFragment.newInstance(chartByDistance);
|
||||
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
|
||||
chartFragment.setTripStatisticsUpdater(TrackStubUtils.INITIAL_TIME);
|
||||
}
|
||||
|
||||
+4
-4
@@ -32,11 +32,11 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
import java.util.SimpleTimeZone;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.data.WaypointsColumns;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.eq;
|
||||
@@ -57,9 +57,9 @@ public abstract class AbstractTestFileTrackImporter {
|
||||
static final double TRACK_ELEVATION = 324.0;
|
||||
|
||||
static final String TRACK_TIME_0 = "2010-04-22T18:21:00Z";
|
||||
static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123";
|
||||
static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123";
|
||||
static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123";
|
||||
static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123Z";
|
||||
static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123Z";
|
||||
static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123Z";
|
||||
|
||||
static final SimpleDateFormat DATE_FORMAT_0 = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US);
|
||||
static final SimpleDateFormat DATE_FORMAT_1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
|
||||
|
||||
+1
-1
@@ -19,10 +19,10 @@ import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
||||
|
||||
|
||||
+11
-11
@@ -42,10 +42,10 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.CustomContentProvider;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
|
||||
import de.dennisguse.opentracks.stats.TripStatistics;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
@@ -95,7 +95,7 @@ public class TrackRecordingServiceTest {
|
||||
@After
|
||||
public void tearDown() throws TimeoutException {
|
||||
// Reset service (if some previous test failed)
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
if (service.isRecording() || service.isPaused()) {
|
||||
service.endCurrentTrack();
|
||||
}
|
||||
@@ -126,7 +126,7 @@ public class TrackRecordingServiceTest {
|
||||
|
||||
Intent startIntent = createStartIntent(context);
|
||||
mServiceRule.startService(startIntent);
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(startIntent));
|
||||
|
||||
// Test if we start in no-recording mode by default.
|
||||
Assert.assertFalse(service.isRecording());
|
||||
@@ -138,7 +138,7 @@ public class TrackRecordingServiceTest {
|
||||
public void testRecording_oldTracks() throws Exception {
|
||||
createDummyTrack(trackId, -1L, false);
|
||||
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
Assert.assertFalse(service.isRecording());
|
||||
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
|
||||
}
|
||||
@@ -147,7 +147,7 @@ public class TrackRecordingServiceTest {
|
||||
@Test
|
||||
public void testRecording_orphanedRecordingTrack() throws Exception {
|
||||
Intent startIntent = createStartIntent(context);
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(startIntent));
|
||||
|
||||
// Just set recording track to a bogus value.
|
||||
// Make sure that the service will not start recording and will clear the bogus track.
|
||||
@@ -161,7 +161,7 @@ public class TrackRecordingServiceTest {
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testStartNewTrack_alreadyRecording() throws Exception {
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
service.startNewTrack();
|
||||
Assert.assertTrue(service.isRecording());
|
||||
long trackId = service.getRecordingTrackId();
|
||||
@@ -178,7 +178,7 @@ public class TrackRecordingServiceTest {
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testEndCurrentTrack_noRecording() throws Exception {
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
Assert.assertFalse(service.isRecording());
|
||||
|
||||
// Ending the current track when there is no recording should not result in any error.
|
||||
@@ -210,7 +210,7 @@ public class TrackRecordingServiceTest {
|
||||
/**
|
||||
* Inserts a location and waits for 200ms.
|
||||
*/
|
||||
private void insertLocation(ITrackRecordingService trackRecordingService) throws InterruptedException {
|
||||
private void insertLocation(TrackRecordingServiceInterface trackRecordingService) throws InterruptedException {
|
||||
Location location = new Location("gps");
|
||||
location.setLongitude(35.0f);
|
||||
location.setLatitude(45.0f);
|
||||
@@ -226,7 +226,7 @@ public class TrackRecordingServiceTest {
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
Assert.assertFalse(service.isRecording());
|
||||
|
||||
long waypointId = service.insertWaypoint(null, null, null, null);
|
||||
@@ -236,7 +236,7 @@ public class TrackRecordingServiceTest {
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testInsertWaypointMarker_validWaypoint() throws Exception {
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
|
||||
service.startNewTrack();
|
||||
Assert.assertTrue(service.isRecording());
|
||||
insertLocation(service);
|
||||
|
||||
+11
-11
@@ -25,9 +25,9 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.CustomContentProvider;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
|
||||
import de.dennisguse.opentracks.stats.TripStatistics;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
@@ -47,7 +47,7 @@ public class TrackRecordingServiceTestLooper {
|
||||
|
||||
private Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
private ContentProviderUtils providerUtils;
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
|
||||
@BeforeClass
|
||||
public static void preSetUp() {
|
||||
@@ -67,26 +67,26 @@ public class TrackRecordingServiceTestLooper {
|
||||
};
|
||||
customContentProvider.attachInfo(context, null);
|
||||
|
||||
providerUtils = new ContentProviderUtils(context);
|
||||
contentProviderUtils = new ContentProviderUtils(context);
|
||||
|
||||
// Let's use default values.
|
||||
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
|
||||
sharedPreferences.edit().clear().apply();
|
||||
|
||||
// Ensure that the database is empty before every test
|
||||
providerUtils.deleteAllTracks(context);
|
||||
contentProviderUtils.deleteAllTracks(context);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws TimeoutException {
|
||||
// Reset service (if some previous test failed)
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
|
||||
if (service.isRecording() || service.isPaused()) {
|
||||
service.endCurrentTrack();
|
||||
}
|
||||
|
||||
// Ensure that the database is empty after every test
|
||||
providerUtils.deleteAllTracks(context);
|
||||
contentProviderUtils.deleteAllTracks(context);
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@@ -186,20 +186,20 @@ public class TrackRecordingServiceTestLooper {
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testIntegration_completeRecordingSession() throws TimeoutException {
|
||||
List<Track> tracks = providerUtils.getAllTracks();
|
||||
List<Track> tracks = contentProviderUtils.getAllTracks();
|
||||
Assert.assertTrue(tracks.isEmpty());
|
||||
fullRecordingSession();
|
||||
}
|
||||
|
||||
private void fullRecordingSession() throws TimeoutException {
|
||||
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
|
||||
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
|
||||
Assert.assertFalse(service.isRecording());
|
||||
|
||||
// Start a track.
|
||||
long id = service.startNewTrack();
|
||||
Assert.assertTrue(id >= 0);
|
||||
Assert.assertTrue(service.isRecording());
|
||||
Track track = providerUtils.getTrack(id);
|
||||
Track track = contentProviderUtils.getTrack(id);
|
||||
Assert.assertNotNull(track);
|
||||
Assert.assertEquals(id, track.getId());
|
||||
Assert.assertEquals(id, PreferencesUtils.getRecordingTrackId(context));
|
||||
@@ -226,7 +226,7 @@ public class TrackRecordingServiceTestLooper {
|
||||
service.endCurrentTrack();
|
||||
Assert.assertFalse(service.isRecording());
|
||||
Assert.assertEquals(-1L, service.getRecordingTrackId());
|
||||
track = providerUtils.getTrack(id);
|
||||
track = contentProviderUtils.getTrack(id);
|
||||
Assert.assertNotNull(track);
|
||||
Assert.assertEquals(id, track.getId());
|
||||
TripStatistics tripStatistics = track.getTripStatistics();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,8 +16,8 @@ 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:versionCode="3287"
|
||||
android:versionName="v3.4.0"> <!-- For FDroid. -->
|
||||
<!-- Permission to initialize services -->
|
||||
<application
|
||||
android:name=".Startup"
|
||||
@@ -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,7 +19,7 @@ package de.dennisguse.opentracks;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
|
||||
/**
|
||||
* Async Task to delete tracks.
|
||||
|
||||
@@ -30,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;
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ import android.widget.AutoCompleteTextView;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,9 +37,9 @@ import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.CursorLoader;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.WaypointsColumns;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
||||
import de.dennisguse.opentracks.util.ActivityUtils;
|
||||
@@ -70,10 +70,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
|
||||
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
private boolean recordingTrackPaused;
|
||||
/*
|
||||
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
|
||||
* class. Anonymous inner class will get garbage collected.
|
||||
*/
|
||||
|
||||
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
@@ -99,7 +96,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
// UI elements
|
||||
private ListView listView;
|
||||
// Callback when an item is selected in the contextual action mode
|
||||
private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
|
||||
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
|
||||
@Override
|
||||
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
|
||||
boolean isSingleSelection = ids.length == 1;
|
||||
|
||||
@@ -40,12 +40,12 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.SortedSet;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.SearchEngine;
|
||||
import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
|
||||
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
||||
@@ -104,7 +104,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
|
||||
private boolean recordingTrackPaused;
|
||||
|
||||
// Callback when an item is selected in the contextual action mode
|
||||
private ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
|
||||
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
|
||||
@Override
|
||||
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
|
||||
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
|
||||
|
||||
@@ -14,7 +14,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
*/
|
||||
public class Startup extends Application {
|
||||
|
||||
private final static String TAG = Startup.class.getSimpleName();
|
||||
private static final String TAG = Startup.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
|
||||
@@ -23,8 +23,8 @@ import android.view.View.OnClickListener;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import de.dennisguse.opentracks.services.ITrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
@@ -61,7 +61,7 @@ public class TrackController {
|
||||
public void run() {
|
||||
if (isResumed && isRecording && !isPaused) {
|
||||
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(System.currentTimeMillis() - totalTimeTimestamp + totalTime));
|
||||
handlerUpdateTotalTime.postDelayed(this, UnitConversions.ONE_SECOND);
|
||||
handlerUpdateTotalTime.postDelayed(this, UnitConversions.ONE_SECOND_MS);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -115,7 +115,7 @@ public class TrackController {
|
||||
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
|
||||
if (!isPaused) {
|
||||
totalTimeTimestamp = System.currentTimeMillis();
|
||||
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND);
|
||||
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ public class TrackController {
|
||||
* Gets the total time for the current recording track.
|
||||
*/
|
||||
private long getTotalTime() {
|
||||
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
return trackRecordingService != null ? trackRecordingService.getTotalTime() : 0L;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
@@ -42,12 +41,11 @@ 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;
|
||||
@@ -113,7 +111,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
}
|
||||
};
|
||||
|
||||
// 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) {
|
||||
@@ -195,7 +192,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
|
||||
trackDataHub = TrackDataHub.newInstance(this);
|
||||
trackDataHub = new TrackDataHub(this);
|
||||
|
||||
FragmentPagerAdapter adapter = new FragmentPagerAdapter(getSupportFragmentManager(), 1) {
|
||||
@Override
|
||||
@@ -207,17 +204,15 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return new StatsFragment();
|
||||
case 1:
|
||||
return new ChartTimeFragment();
|
||||
return ChartFragment.newInstance(false);
|
||||
case 2:
|
||||
return new ChartDistanceFragment();
|
||||
return ChartFragment.newInstance(true);
|
||||
default: //0
|
||||
return new StatsFragment();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
switch (position) {
|
||||
|
||||
@@ -30,8 +30,8 @@ import android.widget.Spinner;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
|
||||
@@ -50,11 +50,11 @@ import androidx.loader.content.Loader;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||
import de.dennisguse.opentracks.services.ITrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||
import de.dennisguse.opentracks.settings.SettingsActivity;
|
||||
import de.dennisguse.opentracks.util.ActivityUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
@@ -127,10 +127,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
|
||||
private boolean recordingTrackPaused;
|
||||
|
||||
/*
|
||||
* Note that sharedPreferenceChangeListener cannot be an anonymous inner class.
|
||||
* Anonymous inner class will get garbage collected.
|
||||
*/
|
||||
private final OnSharedPreferenceChangeListener
|
||||
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
@@ -196,7 +192,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
return;
|
||||
}
|
||||
|
||||
ITrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
|
||||
TrackRecordingServiceInterface service = trackRecordingServiceConnection.getServiceIfBound();
|
||||
if (service == null) {
|
||||
Log.d(TAG, "service not available to start gps or a new recording");
|
||||
return;
|
||||
@@ -409,7 +405,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
trackRecordingServiceConnection.startAndBind(this);
|
||||
bindChangedCallback.run();
|
||||
} else {
|
||||
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
if (trackRecordingService != null) {
|
||||
trackRecordingService.stopGps();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import androidx.annotation.Nullable;
|
||||
*/
|
||||
public class ContentResolverWrapper implements IContentResolver {
|
||||
|
||||
private ContentResolver contentResolver;
|
||||
private final ContentResolver contentResolver;
|
||||
|
||||
public ContentResolverWrapper(ContentResolver contentResolver) {
|
||||
this.contentResolver = contentResolver;
|
||||
|
||||
@@ -122,7 +122,7 @@ public class ChartView extends View {
|
||||
private boolean reportSpeed = true;
|
||||
private boolean showPointer = false;
|
||||
|
||||
private GestureDetectorCompat detectorScrollFlingTab = new GestureDetectorCompat(getContext(), new GestureDetector.SimpleOnGestureListener() {
|
||||
private final GestureDetectorCompat detectorScrollFlingTab = new GestureDetectorCompat(getContext(), new GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
@@ -176,7 +176,7 @@ public class ChartView extends View {
|
||||
}
|
||||
});
|
||||
|
||||
private ScaleGestureDetector detectorZoom = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
private final ScaleGestureDetector detectorZoom = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
|
||||
@@ -18,13 +18,9 @@ package de.dennisguse.opentracks.content;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.database.ContentObserver;
|
||||
import android.net.Uri;
|
||||
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* Data source on the phone.
|
||||
*
|
||||
@@ -33,47 +29,16 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
class DataSource {
|
||||
|
||||
private final ContentResolver contentResolver;
|
||||
private final SharedPreferences sharedPreferences;
|
||||
|
||||
DataSource(Context context) {
|
||||
contentResolver = context.getContentResolver();
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a content observer.
|
||||
*
|
||||
* @param uri the uri
|
||||
* @param observer the observer
|
||||
*/
|
||||
void registerContentObserver(Uri uri, ContentObserver observer) {
|
||||
contentResolver.registerContentObserver(uri, false, observer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a content observer.
|
||||
*
|
||||
* @param observer the observer
|
||||
*/
|
||||
void unregisterContentObserver(ContentObserver observer) {
|
||||
contentResolver.unregisterContentObserver(observer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a shared preference change listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a shared preference change listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
|
||||
}
|
||||
}
|
||||
@@ -38,11 +38,4 @@ public interface DataSourceListener {
|
||||
* Notifies when the track points table is updated.
|
||||
*/
|
||||
void notifyTrackPointsTableUpdated();
|
||||
|
||||
/**
|
||||
* Notifies when a preference changes.
|
||||
*
|
||||
* @param key the preference key
|
||||
*/
|
||||
void notifyPreferenceChanged(String key);
|
||||
}
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
package de.dennisguse.opentracks.content;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.database.ContentObserver;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
@@ -49,7 +47,6 @@ class DataSourceManager {
|
||||
private final TracksTableObserver tracksTableObserver;
|
||||
private final WaypointsTableObserver waypointsTableObserver;
|
||||
private final TrackPointsTableObserver trackPointsTableObserver;
|
||||
private final PreferenceListener preferenceListener;
|
||||
|
||||
DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) {
|
||||
this.dataSource = dataSource;
|
||||
@@ -59,7 +56,6 @@ class DataSourceManager {
|
||||
tracksTableObserver = new TracksTableObserver();
|
||||
waypointsTableObserver = new WaypointsTableObserver();
|
||||
trackPointsTableObserver = new TrackPointsTableObserver();
|
||||
preferenceListener = new PreferenceListener();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,10 +66,7 @@ class DataSourceManager {
|
||||
void updateListeners(EnumSet<TrackDataType> listeners) {
|
||||
EnumSet<TrackDataType> neededListeners = EnumSet.copyOf(listeners);
|
||||
|
||||
/*
|
||||
* Map SAMPLED_OUT_POINT_UPDATES to POINT_UPDATES since they correspond to
|
||||
* the same internal listener
|
||||
*/
|
||||
// Map SAMPLED_OUT_POINT_UPDATES to POINT_UPDATES since they correspond to the same internal listener
|
||||
if (neededListeners.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)) {
|
||||
neededListeners.remove(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE);
|
||||
neededListeners.add(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE);
|
||||
@@ -118,15 +111,11 @@ class DataSourceManager {
|
||||
dataSource.registerContentObserver(WaypointsColumns.CONTENT_URI, waypointsTableObserver);
|
||||
break;
|
||||
case SAMPLED_IN_TRACK_POINTS_TABLE:
|
||||
dataSource.registerContentObserver(
|
||||
TrackPointsColumns.CONTENT_URI, trackPointsTableObserver);
|
||||
dataSource.registerContentObserver(TrackPointsColumns.CONTENT_URI_BY_ID, trackPointsTableObserver);
|
||||
break;
|
||||
case SAMPLED_OUT_TRACK_POINTS_TABLE:
|
||||
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
|
||||
break;
|
||||
case PREFERENCE:
|
||||
dataSource.registerOnSharedPreferenceChangeListener(preferenceListener);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -151,9 +140,6 @@ class DataSourceManager {
|
||||
case SAMPLED_OUT_TRACK_POINTS_TABLE:
|
||||
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
|
||||
break;
|
||||
case PREFERENCE:
|
||||
dataSource.unregisterOnSharedPreferenceChangeListener(preferenceListener);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -218,17 +204,4 @@ class DataSourceManager {
|
||||
dataSourceListener.notifyTrackPointsTableUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for preference changes.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private class PreferenceListener implements OnSharedPreferenceChangeListener {
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
dataSourceListener.notifyPreferenceChanged(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public class DescriptionGenerator {
|
||||
private static final String TEXT_LINE_BREAK = "\n";
|
||||
private static final String TEXT_PARAGRAPH_SEPARATOR = "\n\n";
|
||||
|
||||
private Context context;
|
||||
private final Context context;
|
||||
|
||||
public DescriptionGenerator(Context context) {
|
||||
this.context = context;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package de.dennisguse.opentracks.content;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* A lightweight wrapper around the original {@link Cursor} with a method to clean up.
|
||||
*/
|
||||
public interface LocationIterator extends Iterator<Location>, AutoCloseable {
|
||||
|
||||
/**
|
||||
* Gets the most recently retrieved track point id by {@link #next()}.
|
||||
*/
|
||||
long getLocationId();
|
||||
|
||||
/**
|
||||
* Closes the iterator.
|
||||
*/
|
||||
void close();
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.data.WaypointsColumns;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.stats.TripStatistics;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
@@ -105,7 +106,7 @@ public class SearchEngine {
|
||||
public static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L;
|
||||
|
||||
/**
|
||||
* Comparador for scored results.
|
||||
* Comparator for scored results.
|
||||
*/
|
||||
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR = new Comparator<ScoredResult>() {
|
||||
@Override
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package de.dennisguse.opentracks.content;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.os.Handler;
|
||||
@@ -32,6 +33,9 @@ import java.util.Set;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointFactory;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
@@ -40,11 +44,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackDataHub implements DataSourceListener {
|
||||
|
||||
public synchronized static TrackDataHub newInstance(Context context) {
|
||||
return new TrackDataHub(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACK_POINTS);
|
||||
}
|
||||
public class TrackDataHub implements DataSourceListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
/**
|
||||
* Target number of track points displayed by the map overlay.
|
||||
@@ -77,24 +77,16 @@ public class TrackDataHub implements DataSourceListener {
|
||||
private long selectedTrackId;
|
||||
private long recordingTrackId;
|
||||
private boolean recordingTrackPaused;
|
||||
private boolean metricUnits;
|
||||
private boolean reportSpeed;
|
||||
private int recordingGpsAccuracy;
|
||||
private int recordingDistanceInterval;
|
||||
|
||||
// Track points sampling state
|
||||
private int numLoadedPoints;
|
||||
private long firstSeenLocationId;
|
||||
private long lastSeenLocationId;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the context
|
||||
* @param trackDataManager the track data manager
|
||||
* @param contentProviderUtils the content tracks provider utils
|
||||
* @param targetNumPoints the target number of points
|
||||
*/
|
||||
public TrackDataHub(Context context) {
|
||||
this(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACK_POINTS);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
private TrackDataHub(Context context, TrackDataManager trackDataManager, ContentProviderUtils contentProviderUtils, int targetNumPoints) {
|
||||
this.context = context;
|
||||
@@ -116,7 +108,8 @@ public class TrackDataHub implements DataSourceListener {
|
||||
dataSource = newDataSource();
|
||||
dataSourceManager = new DataSourceManager(dataSource, this);
|
||||
|
||||
notifyPreferenceChanged(null);
|
||||
PreferencesUtils.register(context, this);
|
||||
onSharedPreferenceChanged(null, null);
|
||||
runInHandlerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -133,6 +126,9 @@ public class TrackDataHub implements DataSourceListener {
|
||||
Log.i(TAG, "TrackDataHub not started, ignoring stop.");
|
||||
return;
|
||||
}
|
||||
|
||||
PreferencesUtils.unregister(context, this);
|
||||
|
||||
started = false;
|
||||
|
||||
dataSourceManager.unregisterAllListeners();
|
||||
@@ -253,7 +249,7 @@ public class TrackDataHub implements DataSourceListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyPreferenceChanged(final String key) {
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key) {
|
||||
runInHandlerThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -263,46 +259,6 @@ public class TrackDataHub implements DataSourceListener {
|
||||
if (PreferencesUtils.isKey(context, R.string.recording_track_paused_key, key)) {
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(context);
|
||||
}
|
||||
if (PreferencesUtils.isKey(context, R.string.stats_units_key, key)) {
|
||||
metricUnits = PreferencesUtils.isMetricUnits(context);
|
||||
if (key != null) {
|
||||
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
if (trackDataListener.onMetricUnitsChanged(metricUnits)) {
|
||||
loadDataForListener(trackDataListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (PreferencesUtils.isKey(context, R.string.stats_rate_key, key)) {
|
||||
reportSpeed = PreferencesUtils.isReportSpeed(context);
|
||||
if (key != null) {
|
||||
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
if (trackDataListener.onReportSpeedChanged(reportSpeed)) {
|
||||
loadDataForListener(trackDataListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (PreferencesUtils.isKey(context, R.string.recording_gps_accuracy_key, key)) {
|
||||
recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
|
||||
if (key != null) {
|
||||
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
if (trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy)) {
|
||||
loadDataForListener(trackDataListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
|
||||
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(context);
|
||||
if (key != null) {
|
||||
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
if (trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval)) {
|
||||
loadDataForListener(trackDataListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -316,13 +272,6 @@ public class TrackDataHub implements DataSourceListener {
|
||||
return;
|
||||
}
|
||||
|
||||
for (TrackDataListener trackDataListener : trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
trackDataListener.onMetricUnitsChanged(metricUnits);
|
||||
trackDataListener.onReportSpeedChanged(reportSpeed);
|
||||
trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy);
|
||||
trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval);
|
||||
}
|
||||
|
||||
notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE));
|
||||
|
||||
for (TrackDataListener listener : trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)) {
|
||||
@@ -343,13 +292,6 @@ public class TrackDataHub implements DataSourceListener {
|
||||
Set<TrackDataListener> trackDataListeners = Collections.singleton(trackDataListener);
|
||||
EnumSet<TrackDataType> trackDataTypes = trackDataManager.getTrackDataTypes(trackDataListener);
|
||||
|
||||
if (trackDataTypes.contains(TrackDataType.PREFERENCE)) {
|
||||
trackDataListener.onMetricUnitsChanged(metricUnits);
|
||||
trackDataListener.onReportSpeedChanged(reportSpeed);
|
||||
trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy);
|
||||
trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval);
|
||||
}
|
||||
|
||||
if (trackDataTypes.contains(TrackDataType.TRACKS_TABLE)) {
|
||||
notifyTracksTableUpdate(trackDataListeners);
|
||||
}
|
||||
@@ -450,11 +392,11 @@ public class TrackDataHub implements DataSourceListener {
|
||||
int samplingFrequency = -1;
|
||||
boolean includeNextPoint = false;
|
||||
|
||||
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
|
||||
try (TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY)) {
|
||||
|
||||
while (locationIterator.hasNext()) {
|
||||
Location location = locationIterator.next();
|
||||
long locationId = locationIterator.getLocationId();
|
||||
long locationId = locationIterator.getTrackPointId();
|
||||
|
||||
// Stop if past the last wanted point
|
||||
if (maxPointId != -1L && locationId > maxPointId) {
|
||||
@@ -542,32 +484,4 @@ public class TrackDataHub implements DataSourceListener {
|
||||
public long getSelectedTrackId() {
|
||||
return selectedTrackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the recordingGpsAccuracy.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
int getRecordingGpsAccuracy() {
|
||||
return recordingGpsAccuracy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the metricUnits.
|
||||
*
|
||||
* @return the metricUnits
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean isMetricUnits() {
|
||||
return metricUnits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the reportSpeed.
|
||||
*
|
||||
* @return the reportSpeed
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean isReportSpeed() {
|
||||
return reportSpeed;
|
||||
}
|
||||
}
|
||||
@@ -77,36 +77,4 @@ public interface TrackDataListener {
|
||||
* This gets called after every batch of calls to {@link #clearWaypoints()} and {@link #onNewWaypoint(Waypoint)}.
|
||||
*/
|
||||
void onNewWaypointsDone();
|
||||
|
||||
/**
|
||||
* Called when the metric units preference value is change.
|
||||
*
|
||||
* @param metricUnits true to use metric units, false to use imperial units
|
||||
* @return true to reload all the data, false otherwise.
|
||||
*/
|
||||
boolean onMetricUnitsChanged(boolean metricUnits);
|
||||
|
||||
/**
|
||||
* Called when the report speed preference value is changed.
|
||||
*
|
||||
* @param reportSpeed true to report speed, false to report pace
|
||||
* @return true to reload all the data, false otherwise.
|
||||
*/
|
||||
boolean onReportSpeedChanged(boolean reportSpeed);
|
||||
|
||||
/**
|
||||
* Called when the recording gps accuracy preference value is changed.
|
||||
*
|
||||
* @param recordingGpsAccuracy the new value
|
||||
* @return true to reload all the data, false otherwise
|
||||
*/
|
||||
boolean onRecordingGpsAccuracy(int recordingGpsAccuracy);
|
||||
|
||||
/**
|
||||
* Called when the recording distance interval preference value is changed.
|
||||
*
|
||||
* @param recordingDistanceInterval the new value
|
||||
* @return true to reload all the data, false otherwise.
|
||||
*/
|
||||
boolean onRecordingDistanceIntervalChanged(int recordingDistanceInterval);
|
||||
}
|
||||
|
||||
@@ -26,5 +26,4 @@ public enum TrackDataType {
|
||||
WAYPOINTS_TABLE, // waypoints table changes
|
||||
SAMPLED_IN_TRACK_POINTS_TABLE, // sampled-in track points table changes
|
||||
SAMPLED_OUT_TRACK_POINTS_TABLE, // sampled-out track points table changes
|
||||
PREFERENCE // preference changes
|
||||
}
|
||||
@@ -18,6 +18,8 @@ package de.dennisguse.opentracks.content.data;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -106,6 +108,7 @@ public class Track {
|
||||
this.tripStatistics = tripStatistics;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void addLocation(Location location) {
|
||||
locations.add(location);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+88
-208
@@ -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;
|
||||
@@ -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;
|
||||
public static final String ID_SEPARATOR = ",";
|
||||
|
||||
private final IContentResolver contentResolver;
|
||||
private int defaultCursorBatchSize = 2000;
|
||||
@@ -109,8 +109,6 @@ 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);
|
||||
@@ -185,13 +183,13 @@ public class ContentProviderUtils {
|
||||
* Deletes all tracks (including waypoints and track points).
|
||||
*/
|
||||
public void deleteAllTracks(Context context) {
|
||||
contentResolver.delete(TrackPointsColumns.CONTENT_URI, null, null);
|
||||
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, null, null);
|
||||
contentResolver.delete(WaypointsColumns.CONTENT_URI, null, null);
|
||||
// Delete tracks last since it triggers a database vaccum call
|
||||
contentResolver.delete(TracksColumns.CONTENT_URI, null, null);
|
||||
|
||||
File dir = FileUtils.getPhotoDir(context);
|
||||
deleteDirectoryRecurse(context, dir);
|
||||
deleteDirectoryRecurse(dir);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -216,11 +214,11 @@ public class ContentProviderUtils {
|
||||
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 +226,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 +240,10 @@ public class ContentProviderUtils {
|
||||
* If no track exists, an empty list is returned.
|
||||
* NOTE: the returned tracks do not have any track points attached.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public List<Track> getAllTracks() {
|
||||
ArrayList<Track> tracks = new ArrayList<>();
|
||||
try (Cursor cursor = getTrackCursor(null, null, null, TracksColumns._ID)) {
|
||||
try (Cursor cursor = getTrackCursor(null, null, TracksColumns._ID)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
tracks.ensureCapacity(cursor.getCount());
|
||||
do {
|
||||
@@ -259,7 +258,7 @@ public class ContentProviderUtils {
|
||||
* Gets the last track or null.
|
||||
*/
|
||||
public Track getLastTrack() {
|
||||
try (Cursor cursor = getTrackCursor(null, null, null, TracksColumns.STARTTIME + " DESC")) {
|
||||
try (Cursor cursor = getTrackCursor(null, null, TracksColumns.STARTTIME + " DESC")) {
|
||||
// Using the same order as shown in the track list
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
return createTrack(cursor);
|
||||
@@ -278,7 +277,7 @@ public class ContentProviderUtils {
|
||||
if (trackId < 0) {
|
||||
return null;
|
||||
}
|
||||
try (Cursor cursor = getTrackCursor(null, TracksColumns._ID + "=?", new String[]{Long.toString(trackId)}, TracksColumns._ID)) {
|
||||
try (Cursor cursor = getTrackCursor(TracksColumns._ID + "=?", new String[]{Long.toString(trackId)}, TracksColumns._ID)) {
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
return createTrack(cursor);
|
||||
}
|
||||
@@ -295,7 +294,7 @@ public class ContentProviderUtils {
|
||||
* @param sortOrder the sort order. Can be null
|
||||
*/
|
||||
public Cursor getTrackCursor(String selection, String[] selectionArgs, String sortOrder) {
|
||||
return getTrackCursor(null, selection, selectionArgs, sortOrder);
|
||||
return contentResolver.query(TracksColumns.CONTENT_URI, null, selection, selectionArgs, sortOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,17 +349,6 @@ public class ContentProviderUtils {
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a track cursor.
|
||||
*
|
||||
* @param projection the projection
|
||||
* @param selection the selection
|
||||
* @param selectionArgs the selection arguments
|
||||
* @param sortOrder the sort oder
|
||||
*/
|
||||
private Cursor getTrackCursor(String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
return contentResolver.query(TracksColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a waypoint from a cursor.
|
||||
@@ -448,7 +436,7 @@ public class ContentProviderUtils {
|
||||
* @param waypointId the waypoint id
|
||||
*/
|
||||
|
||||
public void deleteWaypoint(Context context, long waypointId) {
|
||||
public void deleteWaypoint(long waypointId) {
|
||||
final Waypoint waypoint = getWaypoint(waypointId);
|
||||
if (waypoint != null && waypoint.hasPhoto()) {
|
||||
Uri uri = waypoint.getPhotoURI();
|
||||
@@ -464,26 +452,6 @@ public class ContentProviderUtils {
|
||||
contentResolver.delete(WaypointsColumns.CONTENT_URI, WaypointsColumns._ID + "=?", new String[]{Long.toString(waypointId)});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last waypoint for a type. Returns null if it doesn't exist.
|
||||
*
|
||||
* @param trackId the track id
|
||||
*/
|
||||
public Waypoint getLastWaypoint(long trackId) {
|
||||
if (trackId < 0) {
|
||||
return null;
|
||||
}
|
||||
String selection = WaypointsColumns.TRACKID + "=?";
|
||||
String[] selectionArgs = new String[]{Long.toString(trackId)};
|
||||
try (Cursor cursor = getWaypointCursor(null, selection, selectionArgs, WaypointsColumns._ID + " DESC", 1)) {
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return createWaypoint(cursor);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next waypoint number for a type.
|
||||
* Returns -1 if not able to get the next waypoint number.
|
||||
@@ -693,7 +661,7 @@ public class ContentProviderUtils {
|
||||
for (int i = 0; i < length; i++) {
|
||||
values[i] = createContentValues(locations[i], trackId);
|
||||
}
|
||||
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI, values);
|
||||
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -831,102 +799,45 @@ public class ContentProviderUtils {
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new read-only iterator over a given track's points.
|
||||
* It provides a lightweight way of iterating over long tracks without failing due to the underlying cursor limitations.
|
||||
* Since it's a read-only iterator, {@link Iterator#remove()} always throws {@link UnsupportedOperationException}.
|
||||
* Each call to {@link LocationIterator#next()} may advance to the next DB record, and if so, the iterator calls {@link LocationFactory#createLocation()} and populates it with information retrieved from the record.
|
||||
* When done with iteration, {@link LocationIterator#close()} must be called.
|
||||
* Fills a track point from a cursor.
|
||||
*
|
||||
* @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 cursor the cursor pointing to a location.
|
||||
* @param indexes the cached track points indexes
|
||||
* @param location the track point
|
||||
*/
|
||||
public LocationIterator getTrackPointLocationIterator(final long trackId,
|
||||
final long startTrackPointId, final boolean descending,
|
||||
final LocationFactory locationFactory) {
|
||||
if (locationFactory == null) {
|
||||
throw new IllegalArgumentException("locationFactory is null");
|
||||
static void fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes, Location location) {
|
||||
location.reset();
|
||||
|
||||
if (!cursor.isNull(indexes.longitudeIndex)) {
|
||||
location.setLongitude(((double) cursor.getInt(indexes.longitudeIndex)) / 1E6);
|
||||
}
|
||||
return new LocationIterator() {
|
||||
private long lastTrackPointId = -1L;
|
||||
private Cursor cursor = getCursor(startTrackPointId);
|
||||
private final CachedTrackPointsIndexes
|
||||
indexes = cursor != null ? new CachedTrackPointsIndexes(cursor)
|
||||
: null;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Gets the track point cursor.
|
||||
*
|
||||
* @param trackPointId the starting track point id
|
||||
*/
|
||||
private Cursor getCursor(long trackPointId) {
|
||||
return getTrackPointCursor(trackId, trackPointId, defaultCursorBatchSize, descending);
|
||||
}
|
||||
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);
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
};
|
||||
sensorDataSetLocation.setSensorDataSet(new SensorDataSet(heartRate, cadence, power, SensorDataSet.DATA_UNAVAILABLE, location.getTime()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -937,7 +848,7 @@ public class ContentProviderUtils {
|
||||
* @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));
|
||||
return contentResolver.insert(TrackPointsColumns.CONTENT_URI_BY_ID, createContentValues(location, trackId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -984,45 +895,19 @@ public class ContentProviderUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, and if so, the iterator calls {@link TrackPointFactory#createLocation()} and populates it with information retrieved from the record.
|
||||
* When done with iteration, {@link TrackPointIterator#close()} must be called.
|
||||
*
|
||||
* @param 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)
|
||||
* @param trackPointFactory the location factory
|
||||
*/
|
||||
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, final TrackPointFactory trackPointFactory) {
|
||||
return new TrackPointIterator(this, trackId, startTrackPointId, descending, trackPointFactory);
|
||||
}
|
||||
|
||||
private Location findTrackPointBy(String selection, String[] selectionArgs) {
|
||||
@@ -1043,7 +928,11 @@ public class ContentProviderUtils {
|
||||
* @param sortOrder the sort order
|
||||
*/
|
||||
private Cursor getTrackPointCursor(String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
return contentResolver.query(TrackPointsColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
|
||||
return contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, projection, selection, selectionArgs, sortOrder);
|
||||
}
|
||||
|
||||
int getDefaultCursorBatchSize() {
|
||||
return defaultCursorBatchSize;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1057,33 +946,24 @@ public class ContentProviderUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* A cache of track points indexes.
|
||||
* Formats an array of IDs as comma separated string value
|
||||
*
|
||||
* @param ids array with IDs
|
||||
* @return comma separated list of ids
|
||||
*/
|
||||
private static class CachedTrackPointsIndexes {
|
||||
final int idIndex;
|
||||
final int longitudeIndex;
|
||||
final int latitudeIndex;
|
||||
final int timeIndex;
|
||||
final int altitudeIndex;
|
||||
final int accuracyIndex;
|
||||
final int speedIndex;
|
||||
final int bearingIndex;
|
||||
final int sensorHeartRateIndex;
|
||||
final int sensorCadenceIndex;
|
||||
final int sensorPowerIndex;
|
||||
|
||||
CachedTrackPointsIndexes(Cursor cursor) {
|
||||
idIndex = cursor.getColumnIndex(TrackPointsColumns._ID);
|
||||
longitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.LONGITUDE);
|
||||
latitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.LATITUDE);
|
||||
timeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.TIME);
|
||||
altitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE);
|
||||
accuracyIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ACCURACY);
|
||||
speedIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED);
|
||||
bearingIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING);
|
||||
sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE);
|
||||
sensorCadenceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_CADENCE);
|
||||
sensorPowerIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_POWER);
|
||||
public static String formatIdListForUri(long[] ids) {
|
||||
StringBuilder idsPathSegment = new StringBuilder();
|
||||
for (long id : ids) {
|
||||
if (idsPathSegment.length() > 0) {
|
||||
idsPathSegment.append(ID_SEPARATOR);
|
||||
}
|
||||
idsPathSegment.append(id);
|
||||
}
|
||||
return idsPathSegment.toString();
|
||||
}
|
||||
|
||||
public static String[] parseTrackIdsFromUri(Uri url) {
|
||||
return TextUtils.split(url.getLastPathSegment(), ID_SEPARATOR);
|
||||
}
|
||||
|
||||
}
|
||||
+30
-54
@@ -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,44 @@
|
||||
package de.dennisguse.opentracks.content.provider;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.data.WaypointsColumns;
|
||||
|
||||
/**
|
||||
* Database helper for creating and upgrading the database.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
@VisibleForTesting
|
||||
static final int DATABASE_VERSION = 23;
|
||||
|
||||
@VisibleForTesting
|
||||
static final String DATABASE_NAME = "database.db";
|
||||
|
||||
public CustomSQLiteOpenHelper(Context context) {
|
||||
this(context, DATABASE_NAME);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public CustomSQLiteOpenHelper(Context context, String databaseName) {
|
||||
super(context, databaseName, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(TrackPointsColumns.CREATE_TABLE);
|
||||
db.execSQL(TracksColumns.CREATE_TABLE);
|
||||
db.execSQL(WaypointsColumns.CREATE_TABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package de.dennisguse.opentracks.content;
|
||||
package de.dennisguse.opentracks.content.provider;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
+5
-6
@@ -1,6 +1,5 @@
|
||||
package de.dennisguse.opentracks.content;
|
||||
package de.dennisguse.opentracks.content.provider;
|
||||
|
||||
import android.location.Location;
|
||||
import android.location.LocationManager;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
@@ -9,14 +8,14 @@ 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 {
|
||||
public class TrackPointFactory {
|
||||
|
||||
/**
|
||||
* The default {@link LocationFactory} which creates a location each time.
|
||||
* The default {@link TrackPointFactory} which creates a location each time.
|
||||
*/
|
||||
public static LocationFactory DEFAULT_LOCATION_FACTORY = new LocationFactory();
|
||||
public static final TrackPointFactory DEFAULT_LOCATION_FACTORY = new TrackPointFactory();
|
||||
|
||||
public Location createLocation() {
|
||||
public TrackPoint createLocation() {
|
||||
return new TrackPoint(LocationManager.GPS_PROVIDER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package de.dennisguse.opentracks.content.provider;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
|
||||
/**
|
||||
* A lightweight wrapper around the original {@link Cursor} with a method to clean up.
|
||||
*/
|
||||
public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
|
||||
|
||||
private static final String TAG = TrackPointIterator.class.getSimpleName();
|
||||
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
private final long trackId;
|
||||
private final boolean descending;
|
||||
private final TrackPointFactory trackPointFactory;
|
||||
private final CachedTrackPointsIndexes indexes;
|
||||
private long lastTrackPointId = -1L;
|
||||
private Cursor cursor;
|
||||
|
||||
|
||||
public TrackPointIterator(ContentProviderUtils contentProviderUtils, long trackId, long startTrackPointId, boolean descending, TrackPointFactory trackPointFactory) {
|
||||
if (trackPointFactory == null) {
|
||||
throw new IllegalArgumentException("trackPointFactory is null");
|
||||
}
|
||||
|
||||
this.contentProviderUtils = contentProviderUtils;
|
||||
this.trackId = trackId;
|
||||
this.descending = descending;
|
||||
this.trackPointFactory = trackPointFactory;
|
||||
|
||||
cursor = getCursor(startTrackPointId);
|
||||
indexes = cursor != null ? new CachedTrackPointsIndexes(cursor)
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the track point cursor.
|
||||
*
|
||||
* @param trackPointId the starting track point id
|
||||
*/
|
||||
private Cursor getCursor(long trackPointId) {
|
||||
return contentProviderUtils.getTrackPointCursor(trackId, trackPointId, contentProviderUtils.getDefaultCursorBatchSize(), descending);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the cursor to the next batch. Returns true if successful.
|
||||
*/
|
||||
private boolean advanceCursorToNextBatch() {
|
||||
long trackPointId = lastTrackPointId == -1L ? -1L : lastTrackPointId + (descending ? -1 : 1);
|
||||
Log.d(TAG, "Advancing track point id: " + trackPointId);
|
||||
cursor.close();
|
||||
cursor = getCursor(trackPointId);
|
||||
return cursor != null;
|
||||
}
|
||||
|
||||
public long getTrackPointId() {
|
||||
return lastTrackPointId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
if (cursor == null) {
|
||||
return false;
|
||||
}
|
||||
if (cursor.isAfterLast()) {
|
||||
return false;
|
||||
}
|
||||
if (cursor.isLast()) {
|
||||
if (cursor.getCount() != contentProviderUtils.getDefaultCursorBatchSize()) {
|
||||
return false;
|
||||
}
|
||||
return advanceCursorToNextBatch() && !cursor.isAfterLast();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackPoint next() {
|
||||
if (cursor == null) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
if (!cursor.moveToNext()) {
|
||||
if (!advanceCursorToNextBatch() || !cursor.moveToNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
}
|
||||
lastTrackPointId = cursor.getLong(indexes.idIndex);
|
||||
TrackPoint trackPoint = trackPointFactory.createLocation();
|
||||
ContentProviderUtils.fillTrackPoint(cursor, indexes, trackPoint);
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
cursor = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ public enum SensorState {
|
||||
case CONNECTED:
|
||||
return c.getString(R.string.sensor_state_connected);
|
||||
case DISCONNECTING:
|
||||
return c.getString(R.string.sensor_state_disconnected);
|
||||
case DISCONNECTED:
|
||||
return c.getString(R.string.sensor_state_disconnected);
|
||||
default:
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
public class ChartDistanceFragment extends ChartFragment {
|
||||
|
||||
public static final String CHART_FRAGMENT_TAG = ChartDistanceFragment.class.getSimpleName();
|
||||
|
||||
public ChartDistanceFragment() {
|
||||
super(true);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -24,7 +25,6 @@ import android.view.ViewGroup;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
@@ -55,9 +55,18 @@ import de.dennisguse.opentracks.util.UnitConversions;
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public abstract class ChartFragment extends Fragment implements TrackDataListener {
|
||||
public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
|
||||
private static final String STATE_CHART_VIEW_BY_DISTANCE_KEY = "chartViewByDistance";
|
||||
private static final String KEY_CHART_VIEW_BY_DISTANCE_KEY = "chartViewByDistance";
|
||||
|
||||
public static Fragment newInstance(boolean chartByDistance) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(KEY_CHART_VIEW_BY_DISTANCE_KEY, chartByDistance);
|
||||
|
||||
ChartFragment chartFragment = new ChartFragment();
|
||||
chartFragment.setArguments(bundle);
|
||||
return chartFragment;
|
||||
}
|
||||
|
||||
private final List<double[]> pendingPoints = new ArrayList<>();
|
||||
|
||||
@@ -73,11 +82,56 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
|
||||
// Modes of operation
|
||||
private boolean chartByDistance;
|
||||
private boolean[] chartShow = new boolean[]{true, true, true, true, true, true};
|
||||
private final boolean[] chartShow = new boolean[]{true, true, true, true, true, true};
|
||||
|
||||
// UI elements
|
||||
private ChartView chartView;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
if (!isResumed()) {
|
||||
return;
|
||||
}
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key)) {
|
||||
boolean metric = PreferencesUtils.isMetricUnits(getContext());
|
||||
if (metricUnits != metric) {
|
||||
metricUnits = metric;
|
||||
chartView.setMetricUnits(metricUnits);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
chartView.requestLayout();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||
boolean speed = PreferencesUtils.isReportSpeed(getContext());
|
||||
if (reportSpeed != speed) {
|
||||
reportSpeed = speed;
|
||||
chartView.setReportSpeed(reportSpeed);
|
||||
setSeriesEnabled(ChartView.SPEED_SERIES, reportSpeed);
|
||||
setSeriesEnabled(ChartView.PACE_SERIES, !reportSpeed);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
chartView.requestLayout();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.recording_distance_interval_key, key)) {
|
||||
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(getContext());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A runnable that will set the orange pointer as appropriate and redraw.
|
||||
*/
|
||||
@@ -93,14 +147,13 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
}
|
||||
};
|
||||
|
||||
public ChartFragment(boolean chartByDistance) {
|
||||
this.chartByDistance = chartByDistance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
chartByDistance = getArguments().getBoolean(KEY_CHART_VIEW_BY_DISTANCE_KEY, chartByDistance);
|
||||
|
||||
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceIntervalDefault(getContext());
|
||||
|
||||
// Create a chartView here to store data thus won't need to reload all the data on every onStart or onResume.
|
||||
@@ -124,6 +177,8 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
resumeTrackDataHub();
|
||||
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
|
||||
|
||||
checkChartSettings();
|
||||
getActivity().runOnUiThread(updateChart);
|
||||
}
|
||||
@@ -132,6 +187,7 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
pauseTrackDataHub();
|
||||
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -141,20 +197,6 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
layout.removeView(chartView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean(STATE_CHART_VIEW_BY_DISTANCE_KEY, chartByDistance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
chartByDistance = savedInstanceState.getBoolean(STATE_CHART_VIEW_BY_DISTANCE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrackUpdated(Track track) {
|
||||
if (isResumed()) {
|
||||
@@ -229,68 +271,6 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMetricUnitsChanged(boolean metric) {
|
||||
if (isResumed()) {
|
||||
if (metricUnits == metric) {
|
||||
return false;
|
||||
}
|
||||
metricUnits = metric;
|
||||
chartView.setMetricUnits(metricUnits);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
chartView.requestLayout();
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onReportSpeedChanged(boolean speed) {
|
||||
if (isResumed()) {
|
||||
if (reportSpeed == speed) {
|
||||
return false;
|
||||
}
|
||||
reportSpeed = speed;
|
||||
chartView.setReportSpeed(reportSpeed);
|
||||
setSeriesEnabled(ChartView.SPEED_SERIES, reportSpeed);
|
||||
setSeriesEnabled(ChartView.PACE_SERIES, !reportSpeed);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
chartView.requestLayout();
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onRecordingGpsAccuracy(int minRequiredAccuracy) {
|
||||
// We don't care.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onRecordingDistanceIntervalChanged(int value) {
|
||||
if (isResumed()) {
|
||||
if (recordingDistanceInterval == value) {
|
||||
return false;
|
||||
}
|
||||
recordingDistanceInterval = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the chart settings.
|
||||
*/
|
||||
@@ -334,7 +314,7 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
|
||||
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
|
||||
TrackDataType.WAYPOINTS_TABLE, TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
|
||||
TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE, TrackDataType.PREFERENCE));
|
||||
TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -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);
|
||||
|
||||
+1
-4
@@ -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,16 @@ public class MarkerDetailFragment extends Fragment {
|
||||
|
||||
private static final String TAG = MarkerDetailFragment.class.getSimpleName();
|
||||
private static final String KEY_MARKER_ID = "markerId";
|
||||
private static final long HIDE_TEXT_DELAY = 4 * UnitConversions.ONE_SECOND;
|
||||
|
||||
private static final long HIDE_TEXT_DELAY = 4 * UnitConversions.ONE_SECOND_MS;
|
||||
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
private Handler handler;
|
||||
private ImageView photoView;
|
||||
private ImageView textGradient;
|
||||
private LinearLayout waypointInfo;
|
||||
private Waypoint waypoint;
|
||||
private Runnable hideText = new Runnable() {
|
||||
private final Runnable hideText = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Animation animation = AnimationUtils.loadAnimation(getActivity(), R.anim.fadeout);
|
||||
@@ -219,7 +221,7 @@ public class MarkerDetailFragment extends Fragment {
|
||||
photoView.setImageResource(MarkerUtils.ICON_ID);
|
||||
}
|
||||
|
||||
setName(R.id.marker_detail_waypoint_name, hasPhoto);
|
||||
setName(hasPhoto);
|
||||
|
||||
TextView category = getView().findViewById(R.id.marker_detail_waypoint_category);
|
||||
ListItemUtils.setTextView(getActivity(), category, StringUtils.getCategory(waypoint.getCategory()), hasPhoto);
|
||||
@@ -227,16 +229,16 @@ public class MarkerDetailFragment extends Fragment {
|
||||
TextView description = getView().findViewById(R.id.marker_detail_waypoint_description);
|
||||
ListItemUtils.setTextView(getActivity(), description, waypoint.getDescription(), hasPhoto);
|
||||
|
||||
setLocation(R.id.marker_detail_waypoint_location, hasPhoto);
|
||||
setLocation(hasPhoto);
|
||||
}
|
||||
|
||||
private void setName(int resId, boolean addShadow) {
|
||||
TextView textView = getView().findViewById(resId);
|
||||
private void setName(boolean addShadow) {
|
||||
TextView textView = getView().findViewById(R.id.marker_detail_waypoint_name);
|
||||
ListItemUtils.setTextView(getActivity(), textView, waypoint.getName(), addShadow);
|
||||
}
|
||||
|
||||
private void setLocation(int resId, boolean addShadow) {
|
||||
TextView textView = getView().findViewById(resId);
|
||||
private void setLocation(boolean addShadow) {
|
||||
TextView textView = getView().findViewById(R.id.marker_detail_waypoint_location);
|
||||
Location location = waypoint.getLocation();
|
||||
String value;
|
||||
if (location == null) {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
@@ -43,8 +44,8 @@ import de.dennisguse.opentracks.content.TrackDataType;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.services.ITrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
|
||||
import de.dennisguse.opentracks.stats.TripStatistics;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
@@ -61,9 +62,9 @@ import de.dennisguse.opentracks.util.UnitConversions;
|
||||
*/
|
||||
public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
|
||||
public static final String STATS_FRAGMENT_TAG = StatsFragment.class.getSimpleName();
|
||||
private static final String STATS_FRAGMENT_TAG = StatsFragment.class.getSimpleName();
|
||||
|
||||
private static final long UI_UPDATE_INTERVAL = UnitConversions.ONE_SECOND;
|
||||
private static final long UI_UPDATE_INTERVAL = UnitConversions.ONE_SECOND_MS;
|
||||
|
||||
private TrackDataHub trackDataHub;
|
||||
private Handler handlerUpdateUI;
|
||||
@@ -75,8 +76,30 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
private TripStatistics lastTripStatistics = null;
|
||||
|
||||
private String category = "";
|
||||
@Deprecated //TODO This should be handled somewhere else; not in the UI.
|
||||
private int recordingGpsAccuracy;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.recording_track_id_key, key)) {
|
||||
recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(getContext());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.stats, container, false);
|
||||
@@ -221,6 +244,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
resumeTrackDataHub();
|
||||
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null);
|
||||
trackRecordingServiceConnection.startConnection(getContext());
|
||||
@@ -232,6 +256,8 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
pauseTrackDataHub();
|
||||
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
|
||||
|
||||
handlerUpdateUI.removeCallbacks(updateUIeachSecond);
|
||||
}
|
||||
|
||||
@@ -370,48 +396,6 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMetricUnitsChanged(final boolean metric) {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onReportSpeedChanged(final boolean speed) {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onRecordingGpsAccuracy(int newValue) {
|
||||
recordingGpsAccuracy = newValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onRecordingDistanceIntervalChanged(int minRecordingDistance) {
|
||||
// We don't care.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the trackDataHub.
|
||||
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
|
||||
@@ -419,8 +403,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
private synchronized void resumeTrackDataHub() {
|
||||
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
|
||||
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
|
||||
TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE,
|
||||
TrackDataType.PREFERENCE));
|
||||
TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -458,7 +441,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
* Tries to fetch most recent {@link SensorDataSet} {@link de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager}.
|
||||
*/
|
||||
private void updateSensorDataUI() {
|
||||
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
|
||||
SensorDataSet sensorDataSet = null;
|
||||
if (trackRecordingService == null) {
|
||||
|
||||
@@ -5,8 +5,8 @@ import android.content.Context;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.io.file.exporter.FileTrackExporter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.GpxTrackWriter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.KmlTrackWriter;
|
||||
@@ -67,7 +67,7 @@ public enum TrackFileFormat {
|
||||
},
|
||||
KMZ_ONLY_TRACK {
|
||||
|
||||
private final static boolean exportPhotos = false;
|
||||
private static final boolean exportPhotos = false;
|
||||
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context, boolean multiple) {
|
||||
@@ -86,10 +86,15 @@ public enum TrackFileFormat {
|
||||
public String getExtension() {
|
||||
return "kmz";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean includesPhotos() {
|
||||
return exportPhotos;
|
||||
}
|
||||
},
|
||||
KMZ_WITH_TRACKDETAIL {
|
||||
|
||||
private final static boolean exportPhotos = false;
|
||||
private static final boolean exportPhotos = false;
|
||||
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context, boolean multiple) {
|
||||
@@ -108,10 +113,16 @@ public enum TrackFileFormat {
|
||||
public String getExtension() {
|
||||
return "kmz";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean includesPhotos() {
|
||||
return exportPhotos;
|
||||
}
|
||||
|
||||
},
|
||||
KMZ_WITH_TRACKDETAIL_AND_SENSORDATA {
|
||||
|
||||
private final static boolean exportPhotos = false;
|
||||
private static final boolean exportPhotos = false;
|
||||
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context, boolean multiple) {
|
||||
@@ -130,10 +141,17 @@ public enum TrackFileFormat {
|
||||
public String getExtension() {
|
||||
return "kmz";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean includesPhotos() {
|
||||
return exportPhotos;
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES {
|
||||
|
||||
private final static boolean exportPhotos = true;
|
||||
private static final boolean exportPhotos = true;
|
||||
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context, boolean multiple) {
|
||||
@@ -152,6 +170,12 @@ public enum TrackFileFormat {
|
||||
public String getExtension() {
|
||||
return "kmz";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean includesPhotos() {
|
||||
return exportPhotos;
|
||||
}
|
||||
|
||||
},
|
||||
GPX {
|
||||
@Override
|
||||
@@ -205,6 +229,13 @@ public enum TrackFileFormat {
|
||||
*/
|
||||
public abstract String getExtension();
|
||||
|
||||
/**
|
||||
* Returns whether the format supports photos.
|
||||
*/
|
||||
public boolean includesPhotos() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of for each format.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
@@ -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,10 +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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,17 +93,12 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
try {
|
||||
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
|
||||
boolean isRecording = PreferencesUtils.isRecording(exportActivity);
|
||||
boolean isPaused = PreferencesUtils.isRecordingTrackPaused(exportActivity);
|
||||
// Get the wake lock if not recording or paused
|
||||
if (!isRecording || isPaused) {
|
||||
wakeLock = SystemUtils.acquireWakeLock(exportActivity, wakeLock);
|
||||
}
|
||||
return saveAllTracks();
|
||||
|
||||
wakeLock = SystemUtils.acquireWakeLock(exportActivity, wakeLock);
|
||||
|
||||
return exportAllTracks();
|
||||
} finally {
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
}
|
||||
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,15 +126,11 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves tracks to one file (uses first track to determine the filename).
|
||||
* Export the track to one file.
|
||||
*
|
||||
* @param tracks the tracks
|
||||
* @param track the track
|
||||
*/
|
||||
private Boolean saveTracks(Track[] tracks) {
|
||||
if (tracks.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private Boolean exportTrack(Track track) {
|
||||
TrackExporterListener trackExporterListener = new TrackExporterListener() {
|
||||
@Override
|
||||
public void onProgressUpdate(int number, int max) {
|
||||
@@ -155,11 +141,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 +173,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
/**
|
||||
* Saves all the tracks.
|
||||
*/
|
||||
private Boolean saveAllTracks() {
|
||||
private Boolean exportAllTracks() {
|
||||
try (Cursor cursor = contentProviderUtils.getTrackCursor(null, null, TracksColumns._ID)) {
|
||||
if (cursor == null) {
|
||||
return false;
|
||||
@@ -195,7 +185,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
}
|
||||
cursor.moveToPosition(i);
|
||||
Track track = contentProviderUtils.createTrack(cursor);
|
||||
if (track != null && saveTracks(new Track[]{track})) {
|
||||
if (track != null && exportTrack(track)) {
|
||||
processedTrackCount++;
|
||||
}
|
||||
publishProgress(i + 1, totalTrackCount);
|
||||
|
||||
@@ -25,12 +25,12 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.OutputStream;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.LocationFactory;
|
||||
import de.dennisguse.opentracks.content.LocationIterator;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointFactory;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
|
||||
/**
|
||||
@@ -126,10 +126,10 @@ public class FileTrackExporter implements TrackExporter {
|
||||
boolean wroteTrack = false;
|
||||
boolean wroteSegment = false;
|
||||
boolean isLastLocationValid = false;
|
||||
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
|
||||
TrackWriterTrackPointFactory trackPointFactory = new TrackWriterTrackPointFactory();
|
||||
int locationNumber = 0;
|
||||
|
||||
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, locationFactory)) {
|
||||
try (TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, trackPointFactory)) {
|
||||
|
||||
while (locationIterator.hasNext()) {
|
||||
if (Thread.interrupted()) {
|
||||
@@ -144,7 +144,7 @@ public class FileTrackExporter implements TrackExporter {
|
||||
boolean isSegmentValid = isLocationValid && isLastLocationValid;
|
||||
if (!wroteTrack && isSegmentValid) {
|
||||
// Found the first two consecutive locations that are valid
|
||||
trackWriter.writeBeginTrack(track, locationFactory.lastLocation);
|
||||
trackWriter.writeBeginTrack(track, trackPointFactory.lastLocation);
|
||||
wroteTrack = true;
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ public class FileTrackExporter implements TrackExporter {
|
||||
wroteSegment = true;
|
||||
|
||||
// Write the previous location, which we had previously skipped
|
||||
trackWriter.writeLocation(locationFactory.lastLocation);
|
||||
trackWriter.writeLocation(trackPointFactory.lastLocation);
|
||||
}
|
||||
|
||||
// Write the current location
|
||||
@@ -169,7 +169,7 @@ public class FileTrackExporter implements TrackExporter {
|
||||
wroteSegment = false;
|
||||
}
|
||||
}
|
||||
locationFactory.swapLocations();
|
||||
trackPointFactory.swapLocations();
|
||||
isLastLocationValid = isLocationValid;
|
||||
}
|
||||
|
||||
@@ -207,12 +207,12 @@ public class FileTrackExporter implements TrackExporter {
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private class TrackWriterLocationFactory extends LocationFactory {
|
||||
Location currentLocation;
|
||||
Location lastLocation;
|
||||
private class TrackWriterTrackPointFactory extends TrackPointFactory {
|
||||
TrackPoint currentLocation;
|
||||
TrackPoint lastLocation;
|
||||
|
||||
@Override
|
||||
public Location createLocation() {
|
||||
public TrackPoint createLocation() {
|
||||
if (currentLocation == null) {
|
||||
currentLocation = new TrackPoint("");
|
||||
}
|
||||
@@ -220,7 +220,7 @@ public class FileTrackExporter implements TrackExporter {
|
||||
}
|
||||
|
||||
void swapLocations() {
|
||||
Location tempLocation = lastLocation;
|
||||
TrackPoint tempLocation = lastLocation;
|
||||
lastLocation = currentLocation;
|
||||
currentLocation = tempLocation;
|
||||
if (currentLocation != null) {
|
||||
|
||||
@@ -26,11 +26,11 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.DescriptionGenerator;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
@@ -65,9 +65,9 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
|
||||
private PrintWriter printWriter;
|
||||
private List<Float> powerList = new ArrayList<>();
|
||||
private List<Float> cadenceList = new ArrayList<>();
|
||||
private List<Float> heartRateList = new ArrayList<>();
|
||||
private final List<Float> powerList = new ArrayList<>();
|
||||
private final List<Float> cadenceList = new ArrayList<>();
|
||||
private final List<Float> heartRateList = new ArrayList<>();
|
||||
|
||||
private Location startLocation;
|
||||
|
||||
@@ -164,7 +164,7 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
if (printWriter != null && exportTrackDetail) {
|
||||
if (waypoint.hasPhoto() && exportPhotos) {
|
||||
float heading = getHeading(waypoint.getTrackId(), waypoint.getLocation());
|
||||
writePhotoOverlay(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), WAYPOINT_STYLE, waypoint.getLocation(), waypoint.getPhotoUrl(), heading);
|
||||
writePhotoOverlay(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), waypoint.getLocation(), waypoint.getPhotoUrl(), heading);
|
||||
} else {
|
||||
writePlacemark(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), WAYPOINT_STYLE, waypoint.getLocation());
|
||||
}
|
||||
@@ -321,16 +321,14 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
|
||||
/**
|
||||
* Writes a photo overlay.
|
||||
*
|
||||
* @param name the name
|
||||
* @param name the name
|
||||
* @param category the category
|
||||
* @param description the description
|
||||
* @param styleName the style name
|
||||
* @param location the location
|
||||
* @param photoUrl the photo url
|
||||
* @param heading the heading
|
||||
*/
|
||||
private void writePhotoOverlay(String name, String category, String description, String styleName, Location location, String photoUrl, float heading) {
|
||||
private void writePhotoOverlay(String name, String category, String description, Location location, String photoUrl, float heading) {
|
||||
if (location != null && exportTrackDetail) {
|
||||
printWriter.println("<PhotoOverlay>");
|
||||
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
|
||||
@@ -343,7 +341,7 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
printWriter.print("<tilt>90</tilt>");
|
||||
printWriter.println("</Camera>");
|
||||
printWriter.println("<TimeStamp><when>" + getTime(location) + "</when></TimeStamp>");
|
||||
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
|
||||
printWriter.println("<styleUrl>#" + KmlTrackWriter.WAYPOINT_STYLE + "</styleUrl>");
|
||||
writeCategory(category);
|
||||
|
||||
if (exportPhotos) {
|
||||
|
||||
@@ -31,9 +31,9 @@ import java.io.OutputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
/**
|
||||
@@ -52,7 +52,7 @@ public class KmzTrackExporter implements TrackExporter {
|
||||
private final TrackExporter fileTrackExporter;
|
||||
private final Track[] tracks;
|
||||
|
||||
private boolean exportPhotos;
|
||||
private final boolean exportPhotos;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
|
||||
+13
-13
@@ -38,12 +38,12 @@ import javax.xml.parsers.SAXParser;
|
||||
import javax.xml.parsers.SAXParserFactory;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.LocationFactory;
|
||||
import de.dennisguse.opentracks.content.LocationIterator;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointFactory;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.stats.TripStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
@@ -160,9 +160,10 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
Waypoint waypoint = null;
|
||||
Location location = null;
|
||||
TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
|
||||
TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
|
||||
@Deprecated // TODO Should not be necessary anymore?
|
||||
TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
|
||||
|
||||
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
|
||||
try (TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY)) {
|
||||
|
||||
while (true) {
|
||||
if (waypoint == null) {
|
||||
@@ -273,7 +274,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
|
||||
//If not the first segment, add a pause separator if there is at least one location in the last segment.
|
||||
if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment != null) {
|
||||
insertLocation(createLocation(TrackRecordingService.PAUSE_LATITUDE, 0.0, 0.0, trackData.lastLocationInCurrentSegment.getTime()));
|
||||
insertLocation(createLocation(TrackPointsColumns.PAUSE_LATITUDE, 0.0, 0.0, trackData.lastLocationInCurrentSegment.getTime()));
|
||||
}
|
||||
trackData.lastLocationInCurrentSegment = null;
|
||||
}
|
||||
@@ -343,8 +344,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
|
||||
if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment == null) {
|
||||
// If not the first segment, add a resume separator before adding the first location.
|
||||
insertLocation(
|
||||
createLocation(TrackRecordingService.RESUME_LATITUDE, 0.0, 0.0, location.getTime()));
|
||||
insertLocation(createLocation(TrackPointsColumns.RESUME_LATITUDE, 0.0, 0.0, location.getTime()));
|
||||
}
|
||||
trackData.lastLocationInCurrentSegment = location;
|
||||
return location;
|
||||
@@ -411,7 +411,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
timeValue = trackData.importTime;
|
||||
} else {
|
||||
try {
|
||||
timeValue = StringUtils.getTime(time);
|
||||
timeValue = StringUtils.parseTime(time);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
|
||||
}
|
||||
@@ -493,7 +493,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
// TODO Why private inner class?
|
||||
private class TrackData {
|
||||
// The current track
|
||||
Track track = new Track();
|
||||
final Track track = new Track();
|
||||
|
||||
// The number of segments processed for the current track
|
||||
int numberOfSegments = 0;
|
||||
@@ -508,10 +508,10 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
TripStatisticsUpdater tripStatisticsUpdater;
|
||||
|
||||
// The import time of the track.
|
||||
long importTime = System.currentTimeMillis();
|
||||
final long importTime = System.currentTimeMillis();
|
||||
|
||||
// The buffered locations
|
||||
Location[] bufferedLocations = new Location[MAX_BUFFERED_LOCATIONS];
|
||||
final Location[] bufferedLocations = new Location[MAX_BUFFERED_LOCATIONS];
|
||||
|
||||
// The number of buffered locations
|
||||
int numBufferedLocations = 0;
|
||||
|
||||
@@ -24,7 +24,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
|
||||
/**
|
||||
* Imports a GPX file.
|
||||
|
||||
@@ -29,8 +29,8 @@ import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
@@ -28,8 +28,8 @@ import org.xml.sax.SAXException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.io.file.exporter.KmlTrackWriter;
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import java.io.InputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.services;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
/**
|
||||
* Handles connection to {@link LocationManager}.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class LocationManagerConnector {
|
||||
|
||||
private final Handler handler;
|
||||
private final LocationManager locationManager;
|
||||
|
||||
public LocationManagerConnector(Context context, Looper looper) {
|
||||
this.handler = new Handler(looper);
|
||||
|
||||
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests location updates.
|
||||
*
|
||||
* @param minTime the minimal time
|
||||
* @param minDistance the minimal distance
|
||||
* @param locationListener the location listener
|
||||
*/
|
||||
public void requestLocationUpdates(final long minTime, final float minDistance, final LocationListener locationListener) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
@SuppressLint("MissingPermission")
|
||||
public void run() {
|
||||
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, minTime, minDistance, locationListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes location updates.
|
||||
*
|
||||
* @param locationListener the location listener
|
||||
*/
|
||||
public void removeLocationUpdates(final LocationListener locationListener) {
|
||||
locationManager.removeUpdates(locationListener);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package de.dennisguse.opentracks.services;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -28,12 +29,10 @@ import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.core.app.TaskStackBuilder;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -42,13 +41,14 @@ import java.util.concurrent.Executors;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackDetailActivity;
|
||||
import de.dennisguse.opentracks.TrackListActivity;
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.CustomContentProvider;
|
||||
import de.dennisguse.opentracks.content.LocationFactory;
|
||||
import de.dennisguse.opentracks.content.LocationIterator;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointFactory;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
|
||||
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
|
||||
@@ -71,35 +71,26 @@ import de.dennisguse.opentracks.util.UnitConversions;
|
||||
*/
|
||||
public class TrackRecordingService extends Service {
|
||||
|
||||
private static final int NOTIFICATION_ID = 123;
|
||||
|
||||
public static final double PAUSE_LATITUDE = 100.0;
|
||||
public static final double RESUME_LATITUDE = 200.0;
|
||||
|
||||
// Anything faster than that (in meters per second) will be considered moving.
|
||||
public static final double MAX_NO_MOVEMENT_SPEED = 0.224;
|
||||
private static final String TAG = TrackRecordingService.class.getSimpleName();
|
||||
// 1 minute in milliseconds
|
||||
private static final long ONE_MINUTE = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
|
||||
|
||||
public static final double MAX_NO_MOVEMENT_SPEED = 0.224;
|
||||
|
||||
// The following variables are set in onCreate:
|
||||
private ExecutorService executorService;
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
private Handler handler;
|
||||
private LocationManagerConnector locationManagerConnector;
|
||||
private LocationManager locationManager;
|
||||
private PeriodicTaskExecutor voiceExecutor;
|
||||
private SharedPreferences sharedPreferences;
|
||||
private TrackRecordingServiceNotificationManager notificationManager;
|
||||
private LocationListenerPolicy locationListenerPolicy;
|
||||
|
||||
private long recordingTrackId;
|
||||
private boolean recordingTrackPaused;
|
||||
private LocationListenerPolicy locationListenerPolicy;
|
||||
private int recordingDistanceInterval;
|
||||
private int maxRecordingDistance;
|
||||
private int recordingGpsAccuracy;
|
||||
private long currentRecordingInterval;
|
||||
|
||||
// The following variables are set when recording:
|
||||
private TripStatisticsUpdater trackTripStatisticsUpdater;
|
||||
// Note that sharedPreferenceChangeListener cannot be an anonymous inner class; anonymous inner class will get garbage collected.
|
||||
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
@@ -124,12 +115,12 @@ public class TrackRecordingService extends Service {
|
||||
int minRecordingInterval = PreferencesUtils.getMinRecordingInterval(context);
|
||||
if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptBatteryLife(context)) {
|
||||
// Choose battery life over moving time accuracy.
|
||||
locationListenerPolicy = new AdaptiveLocationListenerPolicy(30 * UnitConversions.ONE_SECOND, 5 * ONE_MINUTE, 5);
|
||||
locationListenerPolicy = new AdaptiveLocationListenerPolicy(30 * UnitConversions.ONE_SECOND_MS, 5 * UnitConversions.ONE_MINUTE_MS, 5);
|
||||
} else if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptAccuracy(context)) {
|
||||
// Get all the updates.
|
||||
locationListenerPolicy = new AdaptiveLocationListenerPolicy(UnitConversions.ONE_SECOND, 30 * UnitConversions.ONE_SECOND, 0);
|
||||
locationListenerPolicy = new AdaptiveLocationListenerPolicy(UnitConversions.ONE_SECOND_MS, 30 * UnitConversions.ONE_SECOND_MS, 0);
|
||||
} else {
|
||||
locationListenerPolicy = new AbsoluteLocationListenerPolicy(minRecordingInterval * UnitConversions.ONE_SECOND);
|
||||
locationListenerPolicy = new AbsoluteLocationListenerPolicy(minRecordingInterval * UnitConversions.ONE_SECOND_MS);
|
||||
}
|
||||
}
|
||||
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
|
||||
@@ -144,20 +135,21 @@ public class TrackRecordingService extends Service {
|
||||
}
|
||||
};
|
||||
|
||||
// The following variables are set when recording:
|
||||
private WakeLock wakeLock;
|
||||
private BluetoothRemoteSensorManager remoteSensorManager;
|
||||
|
||||
private TripStatisticsUpdater trackTripStatisticsUpdater;
|
||||
private Location lastLocation;
|
||||
private boolean currentSegmentHasLocation;
|
||||
private boolean isIdle; // true if idle
|
||||
private ServiceBinder binder = new ServiceBinder(this);
|
||||
private LocationListener locationListener = new LocationListener() {
|
||||
private boolean isIdle;
|
||||
|
||||
private TrackRecordingServiceBinder binder = new TrackRecordingServiceBinder(this);
|
||||
private final LocationListener locationListener = new LocationListener() {
|
||||
|
||||
@Override
|
||||
public void onLocationChanged(final Location location) {
|
||||
if (locationManagerConnector == null
|
||||
|| executorService == null
|
||||
|| executorService.isShutdown()
|
||||
|| executorService.isTerminated()) {
|
||||
if (executorService == null || executorService.isShutdown() || executorService.isTerminated()) {
|
||||
return;
|
||||
}
|
||||
executorService.submit(new Runnable() {
|
||||
@@ -184,35 +176,21 @@ public class TrackRecordingService extends Service {
|
||||
}
|
||||
};
|
||||
|
||||
private final Runnable registerLocationRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isRecording() && !isPaused()) {
|
||||
registerLocationListener();
|
||||
}
|
||||
handler.postDelayed(this, ONE_MINUTE);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
executorService = Executors.newSingleThreadExecutor();
|
||||
contentProviderUtils = new ContentProviderUtils(this);
|
||||
handler = new Handler();
|
||||
locationManagerConnector = new LocationManagerConnector(this, handler.getLooper());
|
||||
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
||||
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory());
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
|
||||
notificationManager = new TrackRecordingServiceNotificationManager(this);
|
||||
|
||||
// onSharedPreferenceChanged might not set recordingTrackId.
|
||||
recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
|
||||
// Require voiceExecutor and splitExecutor to be created.
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(sharedPreferences, null);
|
||||
|
||||
handler.post(registerLocationRunnable);
|
||||
PreferencesUtils.register(this, sharedPreferenceChangeListener);
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
|
||||
|
||||
// Try to restart the previous recording track in case the service has been restarted by the system, which can sometimes happen.
|
||||
Track track = contentProviderUtils.getTrack(recordingTrackId);
|
||||
@@ -247,11 +225,10 @@ public class TrackRecordingService extends Service {
|
||||
// Reverse order from onCreate
|
||||
showNotification(false); //TODO Why?
|
||||
|
||||
handler.removeCallbacks(registerLocationRunnable);
|
||||
unregisterLocationListener();
|
||||
locationManager = null;
|
||||
|
||||
// unregister sharedPreferences before shutting down splitExecutor and voiceExecutor
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
PreferencesUtils.unregister(this, sharedPreferenceChangeListener);
|
||||
|
||||
try {
|
||||
voiceExecutor.shutdown();
|
||||
@@ -259,14 +236,13 @@ public class TrackRecordingService extends Service {
|
||||
voiceExecutor = null;
|
||||
}
|
||||
|
||||
locationManagerConnector = null;
|
||||
contentProviderUtils = null;
|
||||
|
||||
binder.detachFromService();
|
||||
binder = null;
|
||||
|
||||
// This should be the next to last operation
|
||||
releaseWakeLock();
|
||||
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
|
||||
|
||||
// Shutdown the executorService last to avoid sending events to a dead executor.
|
||||
executorService.shutdown();
|
||||
@@ -281,6 +257,10 @@ public class TrackRecordingService extends Service {
|
||||
return recordingTrackPaused;
|
||||
}
|
||||
|
||||
public long getRecordingTrackId() {
|
||||
return recordingTrackId;
|
||||
}
|
||||
|
||||
public TripStatistics getTripStatistics() {
|
||||
if (trackTripStatisticsUpdater == null) {
|
||||
return null;
|
||||
@@ -288,6 +268,16 @@ public class TrackRecordingService extends Service {
|
||||
return trackTripStatisticsUpdater.getTripStatistics();
|
||||
}
|
||||
|
||||
public long getTotalTime() {
|
||||
if (trackTripStatisticsUpdater == null) {
|
||||
return 0;
|
||||
}
|
||||
if (!isPaused()) {
|
||||
trackTripStatisticsUpdater.updateTime(System.currentTimeMillis());
|
||||
}
|
||||
return trackTripStatisticsUpdater.getTripStatistics().getTotalTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a waypoint.
|
||||
*
|
||||
@@ -324,7 +314,7 @@ public class TrackRecordingService extends Service {
|
||||
// Insert waypoint
|
||||
Waypoint waypoint = new Waypoint(name, description, category, icon, recordingTrackId, length, duration, location, photoUrl);
|
||||
Uri uri = contentProviderUtils.insertWaypoint(waypoint);
|
||||
return Long.parseLong(uri.getLastPathSegment());
|
||||
return ContentUris.parseId(uri);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,7 +322,7 @@ public class TrackRecordingService extends Service {
|
||||
*
|
||||
* @return the track id
|
||||
*/
|
||||
private long startNewTrack() {
|
||||
long startNewTrack() {
|
||||
if (isRecording()) {
|
||||
Log.d(TAG, "Ignore startNewTrack. Already recording.");
|
||||
return -1L;
|
||||
@@ -343,7 +333,7 @@ public class TrackRecordingService extends Service {
|
||||
// Insert a track
|
||||
Track track = new Track();
|
||||
Uri uri = contentProviderUtils.insertTrack(track);
|
||||
long trackId = Long.parseLong(uri.getLastPathSegment());
|
||||
long trackId = ContentUris.parseId(uri);
|
||||
|
||||
// Update shared preferences
|
||||
updateRecordingState(trackId, false);
|
||||
@@ -358,37 +348,25 @@ public class TrackRecordingService extends Service {
|
||||
track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics());
|
||||
contentProviderUtils.updateTrack(track);
|
||||
|
||||
startRecording(true);
|
||||
startRecording();
|
||||
return trackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart a track.
|
||||
*
|
||||
* @param track the track
|
||||
*/
|
||||
private void restartTrack(Track track) {
|
||||
Log.d(TAG, "Restarting track: " + track.getId());
|
||||
|
||||
TripStatistics tripStatistics = track.getTripStatistics();
|
||||
trackTripStatisticsUpdater = new TripStatisticsUpdater(tripStatistics.getStartTime());
|
||||
|
||||
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
|
||||
|
||||
while (locationIterator.hasNext()) {
|
||||
Location location = locationIterator.next();
|
||||
trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
|
||||
}
|
||||
try (TrackPointIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, TrackPointFactory.DEFAULT_LOCATION_FACTORY)) {
|
||||
trackTripStatisticsUpdater.addLocation(locationIterator, recordingDistanceInterval);
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, "RuntimeException", e);
|
||||
}
|
||||
startRecording(true);
|
||||
startRecording();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes current track.
|
||||
*/
|
||||
private void resumeCurrentTrack() {
|
||||
void resumeCurrentTrack() {
|
||||
if (!isRecording() || !isPaused()) {
|
||||
Log.d(TAG, "Ignore resumeCurrentTrack. Not recording or not paused.");
|
||||
return;
|
||||
@@ -403,20 +381,18 @@ public class TrackRecordingService extends Service {
|
||||
if (track != null) {
|
||||
Location resume = new Location(LocationManager.GPS_PROVIDER);
|
||||
resume.setLongitude(0);
|
||||
resume.setLatitude(RESUME_LATITUDE);
|
||||
resume.setLatitude(TrackPointsColumns.RESUME_LATITUDE);
|
||||
resume.setTime(System.currentTimeMillis());
|
||||
insertLocation(track, resume, null);
|
||||
}
|
||||
|
||||
startRecording(false);
|
||||
startRecording();
|
||||
}
|
||||
|
||||
/**
|
||||
* Common code for starting a new track, resuming a track, or restarting after phone reboot.
|
||||
*
|
||||
* @param trackStarted true if track is started, false if track is resumed
|
||||
*/
|
||||
private void startRecording(boolean trackStarted) {
|
||||
private void startRecording() {
|
||||
// Update instance variables
|
||||
remoteSensorManager = new BluetoothRemoteSensorManager(this);
|
||||
remoteSensorManager.start();
|
||||
@@ -430,19 +406,19 @@ public class TrackRecordingService extends Service {
|
||||
voiceExecutor.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts gps.
|
||||
*/
|
||||
void tryStartGps() {
|
||||
if (isRecording()) return;
|
||||
|
||||
startGps();
|
||||
}
|
||||
|
||||
private void startGps() {
|
||||
wakeLock = SystemUtils.acquireWakeLock(this, wakeLock);
|
||||
registerLocationListener();
|
||||
showNotification(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the current track.
|
||||
*/
|
||||
private void endCurrentTrack() {
|
||||
void endCurrentTrack() {
|
||||
if (!isRecording()) {
|
||||
Log.d(TAG, "Ignore endCurrentTrack. Not recording.");
|
||||
return;
|
||||
@@ -475,10 +451,7 @@ public class TrackRecordingService extends Service {
|
||||
endRecording(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the current track.
|
||||
*/
|
||||
private void pauseCurrentTrack() {
|
||||
void pauseCurrentTrack() {
|
||||
if (!isRecording() || isPaused()) {
|
||||
Log.d(TAG, "Ignore pauseCurrentTrack. Not recording or paused.");
|
||||
return;
|
||||
@@ -495,7 +468,7 @@ public class TrackRecordingService extends Service {
|
||||
|
||||
Location pause = new Location(LocationManager.GPS_PROVIDER);
|
||||
pause.setLongitude(0);
|
||||
pause.setLatitude(PAUSE_LATITUDE);
|
||||
pause.setLatitude(TrackPointsColumns.PAUSE_LATITUDE);
|
||||
pause.setTime(System.currentTimeMillis());
|
||||
insertLocation(track, pause, null);
|
||||
}
|
||||
@@ -527,13 +500,15 @@ public class TrackRecordingService extends Service {
|
||||
/**
|
||||
* Stops gps.
|
||||
*
|
||||
* @param stop true to stop self
|
||||
* @param shutdown true to shutdown self
|
||||
*/
|
||||
private void stopGps(boolean stop) {
|
||||
void stopGps(boolean shutdown) {
|
||||
if (!isRecording()) return;
|
||||
|
||||
unregisterLocationListener();
|
||||
showNotification(false);
|
||||
releaseWakeLock();
|
||||
if (stop) {
|
||||
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
|
||||
if (shutdown) {
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
@@ -564,107 +539,94 @@ public class TrackRecordingService extends Service {
|
||||
PreferencesUtils.setBoolean(this, R.string.recording_track_paused_key, recordingTrackPaused);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when location changed.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
private void onLocationChangedAsync(Location location) {
|
||||
try {
|
||||
if (!isRecording() || isPaused()) {
|
||||
Log.w(TAG, "Ignore onLocationChangedAsync. Not recording or paused.");
|
||||
return;
|
||||
}
|
||||
|
||||
Track track = contentProviderUtils.getTrack(recordingTrackId);
|
||||
if (track == null) {
|
||||
Log.w(TAG, "Ignore onLocationChangedAsync. No track.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LocationUtils.isValidLocation(location)) {
|
||||
Log.w(TAG, "Ignore onLocationChangedAsync. location is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
notificationManager.updateLocation(this, location, recordingGpsAccuracy);
|
||||
|
||||
if (!location.hasAccuracy() || location.getAccuracy() >= recordingGpsAccuracy) {
|
||||
Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy.");
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO Necessary?
|
||||
// Fix for phones that do not set the time field
|
||||
if (location.getTime() == 0L) {
|
||||
location.setTime(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
Location lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
|
||||
long idleTime = 0L;
|
||||
if (lastValidTrackPoint != null && location.getTime() > lastValidTrackPoint.getTime()) {
|
||||
idleTime = location.getTime() - lastValidTrackPoint.getTime();
|
||||
}
|
||||
locationListenerPolicy.updateIdleTime(idleTime);
|
||||
if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) {
|
||||
registerLocationListener();
|
||||
}
|
||||
|
||||
SensorDataSet sensorDataSet = getSensorDataSet();
|
||||
if (sensorDataSet != null) {
|
||||
location = new TrackPoint(location, sensorDataSet);
|
||||
}
|
||||
|
||||
// Always insert the first segment location
|
||||
if (!currentSegmentHasLocation) {
|
||||
insertLocation(track, location, null);
|
||||
currentSegmentHasLocation = true;
|
||||
lastLocation = location;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LocationUtils.isValidLocation(lastValidTrackPoint)) {
|
||||
// Should not happen. The current segment should have a location. Just insert the current location.
|
||||
insertLocation(track, location, null);
|
||||
lastLocation = location;
|
||||
return;
|
||||
}
|
||||
|
||||
double distanceToLastTrackLocation = location.distanceTo(lastValidTrackPoint);
|
||||
if (distanceToLastTrackLocation > maxRecordingDistance) {
|
||||
insertLocation(track, lastLocation, lastValidTrackPoint);
|
||||
|
||||
Location pause = new Location(LocationManager.GPS_PROVIDER);
|
||||
pause.setLongitude(0);
|
||||
pause.setLatitude(PAUSE_LATITUDE);
|
||||
pause.setTime(lastLocation.getTime());
|
||||
insertLocation(track, pause, null);
|
||||
|
||||
insertLocation(track, location, null);
|
||||
isIdle = false;
|
||||
} else if (sensorDataSet != null || distanceToLastTrackLocation >= recordingDistanceInterval) {
|
||||
insertLocation(track, lastLocation, lastValidTrackPoint);
|
||||
insertLocation(track, location, null);
|
||||
isIdle = false;
|
||||
} else if (!isIdle && location.hasSpeed() && location.getSpeed() < MAX_NO_MOVEMENT_SPEED) {
|
||||
insertLocation(track, lastLocation, lastValidTrackPoint);
|
||||
insertLocation(track, location, null);
|
||||
isIdle = true;
|
||||
} else if (isIdle && location.hasSpeed() && location.getSpeed() >= MAX_NO_MOVEMENT_SPEED) {
|
||||
insertLocation(track, lastLocation, lastValidTrackPoint);
|
||||
insertLocation(track, location, null);
|
||||
isIdle = false;
|
||||
} else {
|
||||
Log.d(TAG, "Not recording location, idle");
|
||||
}
|
||||
lastLocation = location;
|
||||
} catch (Error e) {
|
||||
Log.e(TAG, "Error in onLocationChangedAsync", e);
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, "RuntimeException in onLocationChangedAsync", e);
|
||||
throw e;
|
||||
void onLocationChangedAsync(Location location) {
|
||||
if (!isRecording() || isPaused()) {
|
||||
Log.w(TAG, "Ignore onLocationChangedAsync. Not recording or paused.");
|
||||
return;
|
||||
}
|
||||
|
||||
Track track = contentProviderUtils.getTrack(recordingTrackId);
|
||||
if (track == null) {
|
||||
Log.w(TAG, "Ignore onLocationChangedAsync. No track.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LocationUtils.isValidLocation(location)) {
|
||||
Log.w(TAG, "Ignore onLocationChangedAsync. location is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
notificationManager.updateLocation(this, location, recordingGpsAccuracy);
|
||||
|
||||
if (!location.hasAccuracy() || location.getAccuracy() >= recordingGpsAccuracy) {
|
||||
Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy.");
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO Necessary?
|
||||
// Fix for phones that do not set the time field
|
||||
if (location.getTime() == 0L) {
|
||||
location.setTime(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
Location lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId());
|
||||
long idleTime = 0L;
|
||||
if (lastValidTrackPoint != null && location.getTime() > lastValidTrackPoint.getTime()) {
|
||||
idleTime = location.getTime() - lastValidTrackPoint.getTime();
|
||||
}
|
||||
locationListenerPolicy.updateIdleTime(idleTime);
|
||||
if (currentRecordingInterval != locationListenerPolicy.getDesiredPollingInterval()) {
|
||||
registerLocationListener();
|
||||
}
|
||||
|
||||
SensorDataSet sensorDataSet = getSensorDataSet();
|
||||
if (sensorDataSet != null) {
|
||||
location = new TrackPoint(location, sensorDataSet);
|
||||
}
|
||||
|
||||
// Always insert the first segment location
|
||||
if (!currentSegmentHasLocation) {
|
||||
insertLocation(track, location, null);
|
||||
currentSegmentHasLocation = true;
|
||||
lastLocation = location;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LocationUtils.isValidLocation(lastValidTrackPoint)) {
|
||||
// Should not happen. The current segment should have a location. Just insert the current location.
|
||||
insertLocation(track, location, null);
|
||||
lastLocation = location;
|
||||
return;
|
||||
}
|
||||
|
||||
double distanceToLastTrackLocation = location.distanceTo(lastValidTrackPoint);
|
||||
if (distanceToLastTrackLocation > maxRecordingDistance) {
|
||||
insertLocation(track, lastLocation, lastValidTrackPoint);
|
||||
|
||||
Location pause = new Location(LocationManager.GPS_PROVIDER);
|
||||
pause.setLongitude(0);
|
||||
pause.setLatitude(TrackPointsColumns.PAUSE_LATITUDE);
|
||||
pause.setTime(lastLocation.getTime());
|
||||
insertLocation(track, pause, null);
|
||||
|
||||
insertLocation(track, location, null);
|
||||
isIdle = false;
|
||||
} else if (sensorDataSet != null || distanceToLastTrackLocation >= recordingDistanceInterval) {
|
||||
insertLocation(track, lastLocation, lastValidTrackPoint);
|
||||
insertLocation(track, location, null);
|
||||
isIdle = false;
|
||||
} else if (!isIdle && location.hasSpeed() && location.getSpeed() < MAX_NO_MOVEMENT_SPEED) {
|
||||
insertLocation(track, lastLocation, lastValidTrackPoint);
|
||||
insertLocation(track, location, null);
|
||||
isIdle = true;
|
||||
} else if (isIdle && location.hasSpeed() && location.getSpeed() >= MAX_NO_MOVEMENT_SPEED) {
|
||||
insertLocation(track, lastLocation, lastValidTrackPoint);
|
||||
insertLocation(track, location, null);
|
||||
isIdle = false;
|
||||
} else {
|
||||
Log.d(TAG, "Not recording location, idle");
|
||||
}
|
||||
lastLocation = location;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -686,7 +648,7 @@ public class TrackRecordingService extends Service {
|
||||
}
|
||||
|
||||
try {
|
||||
Uri uri = contentProviderUtils.insertTrackPoint(location, track.getId());
|
||||
contentProviderUtils.insertTrackPoint(location, track.getId());
|
||||
trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
|
||||
updateRecordingTrack(track, LocationUtils.isValidLocation(location));
|
||||
} catch (SQLiteException e) {
|
||||
@@ -700,7 +662,7 @@ public class TrackRecordingService extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the recording track time as well as the startId and the stopId.
|
||||
* Updates the recording track time.
|
||||
* Increase the number of points if it is a new and valid track point.
|
||||
*
|
||||
* @param track the track
|
||||
@@ -716,57 +678,37 @@ public class TrackRecordingService extends Service {
|
||||
contentProviderUtils.updateTrack(track);
|
||||
}
|
||||
|
||||
private SensorDataSet getSensorDataSet() {
|
||||
SensorDataSet getSensorDataSet() {
|
||||
if (remoteSensorManager == null || !remoteSensorManager.isEnabled() || !remoteSensorManager.isSensorDataSetValid()) {
|
||||
return null;
|
||||
}
|
||||
return remoteSensorManager.getSensorDataSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the location listener.
|
||||
*/
|
||||
private void registerLocationListener() {
|
||||
if (locationManagerConnector == null) {
|
||||
if (locationManager == null) {
|
||||
Log.e(TAG, "locationManager is null.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
long interval = locationListenerPolicy.getDesiredPollingInterval();
|
||||
locationManagerConnector.requestLocationUpdates(interval, locationListenerPolicy.getMinDistance_m(), locationListener);
|
||||
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, interval, locationListenerPolicy.getMinDistance_m(), locationListener);
|
||||
currentRecordingInterval = interval;
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, "Could not register location listener.", e);
|
||||
} catch (SecurityException e) {
|
||||
Log.e(TAG, "Could not register location listener; permissions not granted.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the location manager.
|
||||
*/
|
||||
private void unregisterLocationListener() {
|
||||
if (locationManagerConnector == null) {
|
||||
if (locationManager == null) {
|
||||
Log.e(TAG, "locationManager is null.");
|
||||
return;
|
||||
}
|
||||
locationManagerConnector.removeLocationUpdates(locationListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the wake lock.
|
||||
*/
|
||||
private void releaseWakeLock() {
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
wakeLock = null;
|
||||
}
|
||||
locationManager.removeUpdates(locationListener);
|
||||
}
|
||||
|
||||
private void showNotification(boolean isGpsStarted) {
|
||||
if ((isRecording() && isPaused()) || (!isRecording() && !isGpsStarted)) {
|
||||
stopForeground(true);
|
||||
}
|
||||
|
||||
if (isRecording() && !isPaused()) {
|
||||
if (isRecording()) {
|
||||
Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
|
||||
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId);
|
||||
PendingIntent pendingIntent = TaskStackBuilder.create(this)
|
||||
@@ -776,7 +718,7 @@ public class TrackRecordingService extends Service {
|
||||
|
||||
notificationManager.updatePendingIntent(pendingIntent);
|
||||
notificationManager.updateContent(getString(R.string.gps_starting));
|
||||
startForeground(NOTIFICATION_ID, notificationManager.getNotification());
|
||||
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.getNotification());
|
||||
}
|
||||
if (!isRecording() && isGpsStarted) {
|
||||
Intent intent = IntentUtils.newIntent(this, TrackListActivity.class);
|
||||
@@ -786,109 +728,11 @@ public class TrackRecordingService extends Service {
|
||||
|
||||
notificationManager.updatePendingIntent(pendingIntent);
|
||||
notificationManager.updateContent(getString(R.string.gps_starting));
|
||||
|
||||
startForeground(NOTIFICATION_ID, notificationManager.getNotification());
|
||||
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.getNotification());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: There is a bug in Android that leaks Binder instances. This bug is
|
||||
* especially visible if we have a non-static class, as there is no way to
|
||||
* nullify reference to the outer class (the service). A workaround is to use
|
||||
* a static class and explicitly clear service and detach it from the
|
||||
* underlying Binder. With this approach, we minimize the leak to 24 bytes per
|
||||
* each service instance. For more details, see the following bug:
|
||||
* http://code.google.com/p/android/issues/detail?id=6426.
|
||||
*/
|
||||
private static class ServiceBinder extends android.os.Binder implements ITrackRecordingService {
|
||||
private TrackRecordingService trackRecordingService;
|
||||
|
||||
public ServiceBinder(TrackRecordingService trackRecordingService) {
|
||||
this.trackRecordingService = trackRecordingService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startGps() {
|
||||
if (!trackRecordingService.isRecording()) {
|
||||
trackRecordingService.startGps();
|
||||
}
|
||||
}
|
||||
|
||||
public void stopGps() {
|
||||
if (!trackRecordingService.isRecording()) {
|
||||
trackRecordingService.stopGps(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long startNewTrack() {
|
||||
return trackRecordingService.startNewTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pauseCurrentTrack() {
|
||||
trackRecordingService.pauseCurrentTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resumeCurrentTrack() {
|
||||
trackRecordingService.resumeCurrentTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endCurrentTrack() {
|
||||
trackRecordingService.endCurrentTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecording() {
|
||||
return trackRecordingService.isRecording();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPaused() {
|
||||
return trackRecordingService.isPaused();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRecordingTrackId() {
|
||||
return trackRecordingService.recordingTrackId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalTime() {
|
||||
TripStatisticsUpdater updater = trackRecordingService.trackTripStatisticsUpdater;
|
||||
if (updater == null) {
|
||||
return 0;
|
||||
}
|
||||
if (!trackRecordingService.isPaused()) {
|
||||
updater.updateTime(System.currentTimeMillis());
|
||||
}
|
||||
return updater.getTripStatistics().getTotalTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long insertWaypoint(String name, String category, String description, String photoUrl) {
|
||||
return trackRecordingService.insertWaypoint(name, category, description, photoUrl);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Override
|
||||
public void insertTrackPoint(Location location) {
|
||||
trackRecordingService.onLocationChangedAsync(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SensorDataSet getSensorData() {
|
||||
return trackRecordingService.getSensorDataSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches from the track recording service. Clears the reference to the
|
||||
* outer class to minimize the leak.
|
||||
*/
|
||||
private void detachFromService() {
|
||||
trackRecordingService = null;
|
||||
if (!isRecording() && !isGpsStarted) {
|
||||
stopForeground(true);
|
||||
notificationManager.cancelNotification();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package de.dennisguse.opentracks.services;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
|
||||
/**
|
||||
* TODO: There is a bug in Android that leaks Binder instances. This bug is
|
||||
* especially visible if we have a non-static class, as there is no way to
|
||||
* nullify reference to the outer class (the service). A workaround is to use
|
||||
* a static class and explicitly clear service and detach it from the
|
||||
* underlying Binder. With this approach, we minimize the leak to 24 bytes per
|
||||
* each service instance. For more details, see the following bug:
|
||||
* http://code.google.com/p/android/issues/detail?id=6426.
|
||||
*/
|
||||
class TrackRecordingServiceBinder extends android.os.Binder implements TrackRecordingServiceInterface {
|
||||
private TrackRecordingService trackRecordingService;
|
||||
|
||||
TrackRecordingServiceBinder(TrackRecordingService trackRecordingService) {
|
||||
this.trackRecordingService = trackRecordingService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startGps() {
|
||||
trackRecordingService.tryStartGps();
|
||||
}
|
||||
|
||||
public void stopGps() {
|
||||
trackRecordingService.stopGps(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long startNewTrack() {
|
||||
return trackRecordingService.startNewTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pauseCurrentTrack() {
|
||||
trackRecordingService.pauseCurrentTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resumeCurrentTrack() {
|
||||
trackRecordingService.resumeCurrentTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endCurrentTrack() {
|
||||
trackRecordingService.endCurrentTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecording() {
|
||||
return trackRecordingService.isRecording();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPaused() {
|
||||
return trackRecordingService.isPaused();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRecordingTrackId() {
|
||||
return trackRecordingService.getRecordingTrackId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTotalTime() {
|
||||
return trackRecordingService.getTotalTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long insertWaypoint(String name, String category, String description, String photoUrl) {
|
||||
return trackRecordingService.insertWaypoint(name, category, description, photoUrl);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Override
|
||||
public void insertTrackPoint(Location location) {
|
||||
trackRecordingService.onLocationChangedAsync(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SensorDataSet getSensorData() {
|
||||
return trackRecordingService.getSensorDataSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches from the track recording service. Clears the reference to the
|
||||
* outer class to minimize the leak.
|
||||
*/
|
||||
void detachFromService() {
|
||||
trackRecordingService = null;
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -49,7 +49,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
||||
|
||||
private final Runnable callback;
|
||||
|
||||
private ITrackRecordingService trackRecordingService;
|
||||
private TrackRecordingServiceInterface trackRecordingService;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -86,7 +86,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
||||
/**
|
||||
* Gets the track recording service if bound. Returns null otherwise
|
||||
*/
|
||||
public ITrackRecordingService getServiceIfBound() {
|
||||
public TrackRecordingServiceInterface getServiceIfBound() {
|
||||
return trackRecordingService;
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
||||
*
|
||||
* @param value the value
|
||||
*/
|
||||
private void setTrackRecordingService(ITrackRecordingService value) {
|
||||
private void setTrackRecordingService(TrackRecordingServiceInterface value) {
|
||||
trackRecordingService = value;
|
||||
if (callback != null) {
|
||||
callback.run();
|
||||
@@ -123,7 +123,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Failed to bind a death recipient.", e);
|
||||
}
|
||||
setTrackRecordingService((ITrackRecordingService) service);
|
||||
setTrackRecordingService((TrackRecordingServiceInterface) service);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -180,7 +180,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
||||
* Resumes the recording track.
|
||||
*/
|
||||
public void resumeTrack() {
|
||||
ITrackRecordingService service = getServiceIfBound();
|
||||
TrackRecordingServiceInterface service = getServiceIfBound();
|
||||
if (service != null) {
|
||||
service.resumeCurrentTrack();
|
||||
}
|
||||
@@ -190,7 +190,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
||||
* Pauses the recording track.
|
||||
*/
|
||||
public void pauseTrack() {
|
||||
ITrackRecordingService service = getServiceIfBound();
|
||||
TrackRecordingServiceInterface service = getServiceIfBound();
|
||||
if (service != null) {
|
||||
service.pauseCurrentTrack();
|
||||
}
|
||||
@@ -212,7 +212,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
||||
* @return the id of the marker or -1L if none could be created.
|
||||
*/
|
||||
public long addMarker(Context context, String name, String category, String description, String photoUrl) {
|
||||
ITrackRecordingService trackRecordingService = getServiceIfBound();
|
||||
TrackRecordingServiceInterface trackRecordingService = getServiceIfBound();
|
||||
if (trackRecordingService == null) {
|
||||
Log.d(TAG, "Unable to add marker, no track recording service");
|
||||
} else {
|
||||
@@ -238,7 +238,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
|
||||
* @param showEditor true to show the editor
|
||||
*/
|
||||
public void stopRecording(@NonNull Context context, boolean showEditor) {
|
||||
ITrackRecordingService trackRecordingService = getServiceIfBound();
|
||||
TrackRecordingServiceInterface trackRecordingService = getServiceIfBound();
|
||||
if (trackRecordingService == null) {
|
||||
resetRecordingState(context);
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||
* App's service.
|
||||
* This service is the process that actually records and manages tracks.
|
||||
*/
|
||||
public interface ITrackRecordingService {
|
||||
public interface TrackRecordingServiceInterface {
|
||||
|
||||
/**
|
||||
* Starts gps.
|
||||
+9
-4
@@ -20,13 +20,13 @@ import de.dennisguse.opentracks.util.StringUtils;
|
||||
*/
|
||||
class TrackRecordingServiceNotificationManager {
|
||||
|
||||
private final static int NOTIFICATION_ID = 123;
|
||||
static final int NOTIFICATION_ID = 123;
|
||||
|
||||
private final static String CHANNEL_ID = TrackRecordingServiceNotificationManager.class.getSimpleName();
|
||||
private static final String CHANNEL_ID = TrackRecordingServiceNotificationManager.class.getSimpleName();
|
||||
|
||||
private NotificationCompat.Builder notificationBuilder;
|
||||
private final NotificationCompat.Builder notificationBuilder;
|
||||
|
||||
private NotificationManager notificationManager;
|
||||
private final NotificationManager notificationManager;
|
||||
|
||||
private boolean previousLocationWasAccurate = true;
|
||||
|
||||
@@ -76,6 +76,7 @@ class TrackRecordingServiceNotificationManager {
|
||||
|
||||
notificationBuilder.setContentText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
|
||||
updateNotification();
|
||||
|
||||
notificationBuilder.setOnlyAlertOnce(true);
|
||||
}
|
||||
|
||||
@@ -84,6 +85,10 @@ class TrackRecordingServiceNotificationManager {
|
||||
updateNotification();
|
||||
}
|
||||
|
||||
void cancelNotification() {
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
}
|
||||
|
||||
Notification getNotification() {
|
||||
return notificationBuilder.build();
|
||||
}
|
||||
|
||||
+2
-2
@@ -57,9 +57,9 @@ public class BluetoothConnectionManager {
|
||||
private SensorState sensorState;
|
||||
|
||||
private BluetoothGatt bluetoothGatt;
|
||||
private BluetoothDevice bluetoothDevice;
|
||||
private final BluetoothDevice bluetoothDevice;
|
||||
|
||||
private BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
|
||||
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
||||
switch (newState) {
|
||||
|
||||
@@ -61,11 +61,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
|
||||
stop = false;
|
||||
break;
|
||||
case AudioManager.AUDIOFOCUS_LOSS:
|
||||
stop = true;
|
||||
break;
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
||||
stop = true;
|
||||
break;
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
|
||||
stop = true;
|
||||
break;
|
||||
|
||||
@@ -74,7 +74,7 @@ public class BluetoothLeAdapter extends BaseAdapter {
|
||||
|
||||
public class Device {
|
||||
private String name;
|
||||
private String address;
|
||||
private final String address;
|
||||
|
||||
public Device(String name, String address) {
|
||||
this.name = name;
|
||||
|
||||
@@ -32,9 +32,9 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
*/
|
||||
public class BluetoothLePreference extends DialogPreference {
|
||||
|
||||
private final static String TAG = BluetoothLePreference.class.getSimpleName();
|
||||
private static final String TAG = BluetoothLePreference.class.getSimpleName();
|
||||
|
||||
private final static int DEVICE_NONE_RESOURCEID = R.string.value_none;
|
||||
private static final int DEVICE_NONE_RESOURCEID = R.string.value_none;
|
||||
|
||||
public BluetoothLePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
@@ -88,10 +88,10 @@ public class BluetoothLePreference extends DialogPreference {
|
||||
public static class BluetoothLePreferenceDialog extends PreferenceDialogFragmentCompat {
|
||||
|
||||
private int selectedEntryIndex;
|
||||
private BluetoothLeAdapter listAdapter = new BluetoothLeAdapter();
|
||||
private final BluetoothLeAdapter listAdapter = new BluetoothLeAdapter();
|
||||
|
||||
private BluetoothLeScanner scanner = null;
|
||||
private ScanCallback scanCallback = new ScanCallback() {
|
||||
private final ScanCallback scanCallback = new ScanCallback() {
|
||||
@Override
|
||||
public void onScanResult(int callbackType, ScanResult result) {
|
||||
super.onScanResult(callbackType, result);
|
||||
|
||||
@@ -50,10 +50,6 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
|
||||
|
||||
public static class PrefsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
/*
|
||||
* Note that sharedPreferenceChangeListener cannot be an anonymous inner class.
|
||||
* Anonymous inner class will get garbage collected.
|
||||
*/
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
|
||||
@@ -21,10 +21,11 @@ import android.util.Log;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
|
||||
import static de.dennisguse.opentracks.services.TrackRecordingService.MAX_NO_MOVEMENT_SPEED;
|
||||
import static de.dennisguse.opentracks.services.TrackRecordingService.PAUSE_LATITUDE;
|
||||
|
||||
/**
|
||||
* Updater for {@link TripStatistics}.
|
||||
@@ -122,7 +123,7 @@ public class TripStatisticsUpdater {
|
||||
updateTime(location.getTime());
|
||||
if (!LocationUtils.isValidLocation(location)) {
|
||||
// Either pause or resume marker
|
||||
if (location.getLatitude() == PAUSE_LATITUDE) {
|
||||
if (location.getLatitude() == TrackPointsColumns.PAUSE_LATITUDE) {
|
||||
if (lastLocation != null && lastMovingLocation != null && lastLocation != lastMovingLocation) {
|
||||
currentSegment.addTotalDistance(lastMovingLocation.distanceTo(lastLocation));
|
||||
}
|
||||
@@ -178,6 +179,13 @@ public class TripStatisticsUpdater {
|
||||
lastMovingLocation = location;
|
||||
}
|
||||
|
||||
public void addLocation(TrackPointIterator iterator, int minRecordingDistance) {
|
||||
while (iterator.hasNext()) {
|
||||
Location location = iterator.next();
|
||||
addLocation(location, minRecordingDistance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the smoothed elevation over several readings.
|
||||
* The elevation readings is noisy so the smoothed elevation is better than the raw elevation for many tasks.
|
||||
|
||||
@@ -60,7 +60,7 @@ public class BluetoothUtils {
|
||||
while (adapters.isEmpty()) {
|
||||
synchronized (mutex) {
|
||||
try {
|
||||
mutex.wait(UnitConversions.ONE_SECOND);
|
||||
mutex.wait(UnitConversions.ONE_SECOND_MS);
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Interrupted while waiting for default bluetooth adapter", e);
|
||||
}
|
||||
|
||||
@@ -80,19 +80,6 @@ public class FileUtils {
|
||||
return buildUniqueFileName(directory, fileBaseName, extension, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name from a file name, without the extension.
|
||||
*
|
||||
* @param fileName the file name
|
||||
*/
|
||||
public static String getName(String fileName) {
|
||||
int index = fileName.lastIndexOf('.');
|
||||
if (index == -1) {
|
||||
return fileName;
|
||||
}
|
||||
return fileName.substring(0, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension from a file name.
|
||||
* Returns null if there is no extension.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
@@ -10,6 +9,7 @@ import java.util.ArrayList;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
|
||||
/**
|
||||
* Create an {@link Intent} to request showing a Dashboard.
|
||||
@@ -24,11 +24,12 @@ public class IntentDashboardUtils {
|
||||
private IntentDashboardUtils() {
|
||||
}
|
||||
|
||||
public static boolean startDashboardPlugin(Context context, long trackId) {
|
||||
public static boolean startDashboard(Context context, long[] trackIds) {
|
||||
ArrayList<Uri> uris = new ArrayList<>();
|
||||
String trackIdList = ContentProviderUtils.formatIdListForUri(trackIds);
|
||||
|
||||
uris.add(0, ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId));
|
||||
uris.add(1, ContentUris.withAppendedId(TrackPointsColumns.CONTENT_URI, trackId));
|
||||
uris.add(0, Uri.withAppendedPath(TracksColumns.CONTENT_URI, trackIdList));
|
||||
uris.add(1, Uri.withAppendedPath(TrackPointsColumns.CONTENT_URI_BY_TRACKID, trackIdList));
|
||||
|
||||
Intent intent = new Intent(ACTION_DASHBOARD);
|
||||
intent.putParcelableArrayListExtra(ACTION_DASHBOARD_PAYLOAD, uris);
|
||||
@@ -47,4 +48,5 @@ public class IntentDashboardUtils {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,12 +33,12 @@ import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.DescriptionGenerator;
|
||||
import de.dennisguse.opentracks.content.ShareContentProvider;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.ShareContentProvider;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
|
||||
/**
|
||||
@@ -48,7 +48,7 @@ import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
*/
|
||||
public class IntentUtils {
|
||||
|
||||
private final static String TAG = IntentUtils.class.getSimpleName();
|
||||
private static final String TAG = IntentUtils.class.getSimpleName();
|
||||
|
||||
private static final String JPEG_EXTENSION = "jpeg";
|
||||
|
||||
@@ -140,7 +140,7 @@ public class IntentUtils {
|
||||
return;
|
||||
}
|
||||
|
||||
if (IntentDashboardUtils.startDashboardPlugin(context, trackIds[0])) {
|
||||
if (IntentDashboardUtils.startDashboard(context, trackIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -151,9 +151,9 @@ public class IntentUtils {
|
||||
intent.setDataAndType(uriAndMime.first, uriAndMime.second);
|
||||
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
try {
|
||||
if (intent.resolveActivity(context.getPackageManager()) != null) {
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
} else {
|
||||
Toast.makeText(context, context.getString(R.string.app_not_installed_show_on_map), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
@@ -38,21 +39,30 @@ public class PreferencesUtils {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context);
|
||||
}
|
||||
|
||||
public static void register(Context context, SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener) {
|
||||
getSharedPreferences(context).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
}
|
||||
|
||||
public static void unregister(Context context, SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener) {
|
||||
getSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
//NOTE: is at the moment still used to determine if a track is currently recorded; better ask the service directly.
|
||||
//NOTE: This was also used to recover from a reboot, but this data should not be exposed to the whole application.
|
||||
public static final long RECORDING_TRACK_ID_DEFAULT = -1L;
|
||||
|
||||
public static long getRecordingTrackId(Context context) {
|
||||
return PreferencesUtils.getLong(context, R.string.recording_track_id_key, RECORDING_TRACK_ID_DEFAULT);
|
||||
SharedPreferences sharedPreferences = getSharedPreferences(context);
|
||||
return sharedPreferences.getLong(getKey(context, R.string.recording_track_id_key), RECORDING_TRACK_ID_DEFAULT);
|
||||
}
|
||||
|
||||
public static String getDefaultActivity(Context context) {
|
||||
return PreferencesUtils.getString(context, R.string.default_activity_key, context.getString(R.string.default_activity_default));
|
||||
return getString(context, R.string.default_activity_key, context.getString(R.string.default_activity_default));
|
||||
}
|
||||
|
||||
public static void setDefaultActivity(Context context, String newDefaultActivity) {
|
||||
PreferencesUtils.setString(context, R.string.default_activity_key, newDefaultActivity);
|
||||
setString(context, R.string.default_activity_key, newDefaultActivity);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,7 +83,7 @@ public class PreferencesUtils {
|
||||
* @return true if key == null or key belongs to keyId
|
||||
*/
|
||||
public static boolean isKey(Context context, int keyId, String key) {
|
||||
return key == null || key.equals(PreferencesUtils.getKey(context, keyId));
|
||||
return key == null || key.equals(getKey(context, keyId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,6 +144,7 @@ public class PreferencesUtils {
|
||||
* @param keyId the key id
|
||||
* @param value the value
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static void setInt(Context context, int keyId, int value) {
|
||||
SharedPreferences sharedPreferences = getSharedPreferences(context);
|
||||
Editor editor = sharedPreferences.edit();
|
||||
@@ -141,17 +152,6 @@ public class PreferencesUtils {
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a long preference value.
|
||||
*
|
||||
* @param context the context
|
||||
* @param keyId the key id
|
||||
*/
|
||||
private static long getLong(Context context, int keyId, long defaultValue) {
|
||||
SharedPreferences sharedPreferences = getSharedPreferences(context);
|
||||
return sharedPreferences.getLong(getKey(context, keyId), defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a long preference value.
|
||||
*
|
||||
@@ -203,7 +203,7 @@ public class PreferencesUtils {
|
||||
}
|
||||
|
||||
public static boolean isRecordingTrackPaused(Context context) {
|
||||
return PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key, isRecordingTrackPausedDefault(context));
|
||||
return getBoolean(context, R.string.recording_track_paused_key, isRecordingTrackPausedDefault(context));
|
||||
}
|
||||
|
||||
public static boolean isRecordingTrackPausedDefault(Context context) {
|
||||
@@ -212,7 +212,7 @@ public class PreferencesUtils {
|
||||
|
||||
public static void defaultRecordingTrackPaused(Context context) {
|
||||
final boolean RECORDING_TRACK_PAUSED = context.getResources().getBoolean(R.bool.recording_track_paused_default);
|
||||
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, RECORDING_TRACK_PAUSED);
|
||||
setBoolean(context, R.string.recording_track_paused_key, RECORDING_TRACK_PAUSED);
|
||||
}
|
||||
|
||||
public static String getBluetoothHeartRateSensorAddressNone(Context context) {
|
||||
@@ -228,7 +228,7 @@ public class PreferencesUtils {
|
||||
}
|
||||
|
||||
public static String getBluetoothHeartRateSensorAddress(Context context) {
|
||||
return PreferencesUtils.getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothHeartRateSensorAddressNone(context));
|
||||
return getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothHeartRateSensorAddressNone(context));
|
||||
}
|
||||
|
||||
public static boolean shouldShowStatsOnLockscreen(Context context) {
|
||||
@@ -243,21 +243,21 @@ public class PreferencesUtils {
|
||||
|
||||
public static boolean isShowStatsGradeElevation(Context context) {
|
||||
final boolean STATS_SHOW_GRADE_ELEVATION = context.getResources().getBoolean(R.bool.stats_show_grade_elevation_default);
|
||||
return PreferencesUtils.getBoolean(context, R.string.stats_show_grade_elevation_key, STATS_SHOW_GRADE_ELEVATION);
|
||||
return getBoolean(context, R.string.stats_show_grade_elevation_key, STATS_SHOW_GRADE_ELEVATION);
|
||||
}
|
||||
|
||||
public static boolean isStatsShowCoordinate(Context context) {
|
||||
final boolean STATS_SHOW_COORDINATE = context.getResources().getBoolean(R.bool.stats_show_coordinate_default);
|
||||
return PreferencesUtils.getBoolean(context, R.string.stats_show_coordinate_key, STATS_SHOW_COORDINATE);
|
||||
return getBoolean(context, R.string.stats_show_coordinate_key, STATS_SHOW_COORDINATE);
|
||||
}
|
||||
|
||||
public static int getVoiceFrequency(Context context) {
|
||||
final int VOICE_FREQUENCY_DEFAULT = Integer.parseInt(context.getResources().getString(R.string.voice_frequency_default));
|
||||
return PreferencesUtils.getInt(context, R.string.voice_frequency_key, VOICE_FREQUENCY_DEFAULT);
|
||||
return getInt(context, R.string.voice_frequency_key, VOICE_FREQUENCY_DEFAULT);
|
||||
}
|
||||
|
||||
public static int getRecordingDistanceInterval(Context context) {
|
||||
return PreferencesUtils.getInt(context, R.string.recording_distance_interval_key, getRecordingDistanceIntervalDefault(context));
|
||||
return getInt(context, R.string.recording_distance_interval_key, getRecordingDistanceIntervalDefault(context));
|
||||
}
|
||||
|
||||
public static int getRecordingDistanceIntervalDefault(Context context) {
|
||||
@@ -266,12 +266,12 @@ public class PreferencesUtils {
|
||||
|
||||
public static int getMaxRecordingDistance(Context context) {
|
||||
final int MAX_RECORDING_DISTANCE = Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default));
|
||||
return PreferencesUtils.getInt(context, R.string.max_recording_distance_key, MAX_RECORDING_DISTANCE);
|
||||
return getInt(context, R.string.max_recording_distance_key, MAX_RECORDING_DISTANCE);
|
||||
}
|
||||
|
||||
public static int getMinRecordingInterval(Context context) {
|
||||
final int MIN_RECORDING_INTERVAL = Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default));
|
||||
return PreferencesUtils.getInt(context, R.string.min_recording_interval_key, MIN_RECORDING_INTERVAL);
|
||||
return getInt(context, R.string.min_recording_interval_key, MIN_RECORDING_INTERVAL);
|
||||
}
|
||||
|
||||
public static int getMinRecordingIntervalAdaptAccuracy(Context context) {
|
||||
@@ -286,19 +286,20 @@ public class PreferencesUtils {
|
||||
return Integer.parseInt(context.getResources().getString(R.string.min_recording_interval_default));
|
||||
}
|
||||
|
||||
|
||||
public static int getRecordingGPSAccuracy(Context context) {
|
||||
final int RECORDING_GPS_ACCURACY = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_default));
|
||||
return PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key, RECORDING_GPS_ACCURACY);
|
||||
return getInt(context, R.string.recording_gps_accuracy_key, RECORDING_GPS_ACCURACY);
|
||||
}
|
||||
|
||||
public static boolean isRecording(Context context) {
|
||||
long recordingTrackId = PreferencesUtils.getRecordingTrackId(context);
|
||||
return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
long recordingTrackId = getRecordingTrackId(context);
|
||||
return isRecording(recordingTrackId);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
//TODO Method is very misleading: it only checks if the provided trackId not the default value (i.e., not recording).
|
||||
public static boolean isRecording(long recordingTrackId) {
|
||||
return recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
return recordingTrackId != RECORDING_TRACK_ID_DEFAULT;
|
||||
}
|
||||
|
||||
public static void resetPreferences(Context context, boolean readAgain) {
|
||||
|
||||
@@ -17,6 +17,7 @@ package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.Pair;
|
||||
@@ -24,6 +25,9 @@ import android.util.Pair;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.ParsePosition;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
@@ -42,8 +46,16 @@ public class StringUtils {
|
||||
|
||||
private static final String COORDINATE_DEGREE = "\u00B0";
|
||||
|
||||
//TODO Remove when upgrading to API level 26+.
|
||||
@Deprecated
|
||||
private static final SimpleDateFormat ISO_8601_DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
|
||||
|
||||
//TODO Remove when upgrading to API level 26+.
|
||||
@Deprecated
|
||||
private static final SimpleDateFormat ISO_8601_BASE = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
|
||||
|
||||
//TODO Remove when upgrading to API level 26+.
|
||||
@Deprecated
|
||||
private static final Pattern ISO_8601_EXTRAS = Pattern.compile("^(\\.\\d+)?(?:Z|([+-])(\\d{2}):(\\d{2}))?$");
|
||||
|
||||
static {
|
||||
@@ -51,7 +63,6 @@ public class StringUtils {
|
||||
ISO_8601_BASE.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
|
||||
private StringUtils() {
|
||||
}
|
||||
|
||||
@@ -73,7 +84,11 @@ public class StringUtils {
|
||||
* @param time_ms the time in milliseconds
|
||||
*/
|
||||
public static String formatDateTimeIso8601(long time_ms) {
|
||||
return ISO_8601_DATE_TIME_FORMAT.format(time_ms);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
return Instant.ofEpochMilli(time_ms).toString();
|
||||
} else {
|
||||
return ISO_8601_DATE_TIME_FORMAT.format(time_ms);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,8 +284,19 @@ public class StringUtils {
|
||||
*
|
||||
* @param xmlDateTime the XML date time string
|
||||
*/
|
||||
//TODO Can this be replaced using java.time?
|
||||
public static long getTime(String xmlDateTime) {
|
||||
public static long parseTime(String xmlDateTime) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
TemporalAccessor t = DateTimeFormatter.ISO_DATE_TIME.parse(xmlDateTime);
|
||||
return Instant.from(t).toEpochMilli();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Invalid XML dateTime value: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO Remove the following when upgrading to API level 26+.
|
||||
//ATTENTION: The following code does not require a time zone (+01 or Z); while ISO_DATE_TIME requires this!
|
||||
|
||||
// Parse the date time base
|
||||
ParsePosition position = new ParsePosition(0);
|
||||
Date date = ISO_8601_BASE.parse(xmlDateTime, position);
|
||||
|
||||
@@ -99,4 +99,16 @@ public class SystemUtils {
|
||||
}
|
||||
return wakeLock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the wake lock if it is held.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public static WakeLock releaseWakeLock(WakeLock wakeLock) {
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,9 @@ package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Utilities for updating track.
|
||||
@@ -29,7 +29,7 @@ import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
*/
|
||||
public class TrackUtils {
|
||||
|
||||
public static String TRACK_SORT_ORDER = TracksColumns.STARTTIME + " DESC";
|
||||
public static final String TRACK_SORT_ORDER = TracksColumns.STARTTIME + " DESC";
|
||||
|
||||
private TrackUtils() {
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ public class UnitConversions {
|
||||
|
||||
// Time
|
||||
// 1 second in milliseconds
|
||||
public static final long ONE_SECOND = UnitConversions.S_TO_MS;
|
||||
public static final long ONE_SECOND_MS = UnitConversions.S_TO_MS;
|
||||
public static final long ONE_MINUTE_MS = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
|
||||
|
||||
// multiplication factor to convert milliseconds to seconds
|
||||
public static final double MS_TO_S = 1d / S_TO_MS;
|
||||
// multiplication factor to convert minutes to seconds
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="239dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_logo_color_24dp" />
|
||||
|
||||
<TextView
|
||||
@@ -80,6 +80,7 @@
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="web"
|
||||
android:gravity="center"
|
||||
android:text="@string/third_party_libraries" />
|
||||
|
||||
|
||||
@@ -44,20 +44,6 @@
|
||||
android:linksClickable="true"
|
||||
android:text="@string/help_track_map_content" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/help_data_statistics_customized_title"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/help_data_statistics_customized_content" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
@@ -72,34 +58,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/help_recording_content" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/help_gps_imprecise_title"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/help_gps_imprecise_content" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/help_heart_rate_sensors_title"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/help_heart_rate_sensors_content" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
@@ -128,20 +86,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/help_track_export_content" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/help_data_upload_title"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/generic_no" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
@@ -155,20 +99,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/generic_no" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/help_data_online_title"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/generic_no" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
@@ -182,6 +112,36 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/help_data_storage_content" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/help_gps_troubleshooting_title"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:linksClickable="true"
|
||||
android:text="@string/help_gps_troubleshooting_content" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/help_support_title"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:linksClickable="true"
|
||||
android:text="@string/help_support_content" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
@@ -15,16 +15,16 @@ limitations under the License.
|
||||
-->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/list_context_menu_share"
|
||||
android:icon="@drawable/ic_share_24dp"
|
||||
android:title="@string/menu_share"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/list_context_menu_show_on_map"
|
||||
android:icon="@drawable/ic_map_24dp"
|
||||
android:title="@string/menu_show_on_map"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/list_context_menu_share"
|
||||
android:icon="@drawable/ic_share_24dp"
|
||||
android:title="@string/menu_share"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/list_context_menu_edit"
|
||||
android:icon="@drawable/ic_edit_24dp"
|
||||
|
||||
@@ -102,6 +102,8 @@
|
||||
<string name="description_track_segment">Segment</string>
|
||||
<string name="export_all_option">Alle als %1$s</string>
|
||||
<string name="export_all_title">In externen Speicher exportieren</string>
|
||||
<string name="export_with_photos">mit Fotos</string>
|
||||
<string name="export_without_photos">ohne Fotos</string>
|
||||
<string name="export_external_storage">Externer Speicher</string>
|
||||
<string name="export_external_storage_error">%1$d von %2$s in %3$s exportiert</string>
|
||||
<string name="export_external_storage_option">als %1$s in %2$s</string>
|
||||
|
||||
@@ -102,6 +102,8 @@
|
||||
<string name="description_track_segment">Segmento</string>
|
||||
<string name="export_all_option">Todo como %1$s</string>
|
||||
<string name="export_all_title">Exportar a unidad de almacenamiento externa</string>
|
||||
<string name="export_with_photos">con fotos</string>
|
||||
<string name="export_without_photos">sin fotos</string>
|
||||
<string name="export_external_storage">Unidad de almacenamiento externa</string>
|
||||
<string name="export_external_storage_error">Se exportaron %1$d de %2$s a %3$s</string>
|
||||
<string name="export_external_storage_option">como %1$s a %2$s</string>
|
||||
|
||||
@@ -24,43 +24,77 @@ limitations under the License.
|
||||
<!-- Help -->
|
||||
<string name="help_feature_title">OpenTracks has the following features:</string>
|
||||
<string name="help_feature_content">
|
||||
* Create markers (incl. pictures)\n
|
||||
* Heart rate sensor (Bluetooth LE)\n
|
||||
* Voice announcements by time or distance\n
|
||||
* While recording, show on lock screen\n
|
||||
* Share tracks
|
||||
* create GPS tracks\n
|
||||
* use a heart rate sensor (Bluetooth LE only)\n
|
||||
* voice announcements (by time or distance)\n
|
||||
* create markers (incl. pictures)\n
|
||||
* works completely offline\n
|
||||
* show tracks on a map (see below)
|
||||
</string>
|
||||
<string name="help_data_statistics_customized_title">Can the statistics view be customized?</string>
|
||||
<string name="help_data_statistics_customized_content">Yes, it can be customized in the Settings.</string>
|
||||
<string name="help_recording_title">How does OpenTracks record data?</string>
|
||||
<string name="help_recording_content">OpenTracks uses the GPS sensor of the device to record the location continuously.</string>
|
||||
<string name="help_gps_imprecise_title">Statistics (e.g., speed) reported by OpenTracks are imprecise?</string>
|
||||
<string name="help_gps_imprecise_content">OpenTracks directly uses the data provided by the GPS of the mobile device. If the GPS device reports inaccurate locations, OpenTracks cannot do anything about it.</string>
|
||||
<string name="help_heart_rate_sensors_title">Which heart rate sensors does OpenTracks support?</string>
|
||||
<string name="help_heart_rate_sensors_content">OpenTracks should work with any Bluetooth LE heart sensor that support streaming the data in real-time. Smartwatches that store the heart rate data are not supported.</string>
|
||||
|
||||
<string name="help_track_map_title">How to show a track on a map?</string>
|
||||
<string name="help_track_map_content">
|
||||
This requires a 3rd party application to be installed that can consume KMZ-encoded geo data.
|
||||
This works well with <a href="http://osmand.net/">OsmAnd~</a> and <a href="https://maps.me/">Maps.ME</a>.
|
||||
In fact, an App needs to support <i>ACTION_VIEW</i> with MIME <i>application/vnd.google-earth.kmz</i>.
|
||||
At the moment, the recorded locations as well as the timestamps are send to an App while pictures and sensor data are not shared.</string>
|
||||
Use the small map icon.\n
|
||||
a)
|
||||
(live updates) Install <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>.
|
||||
It provides online or offline maps via <a href="https://www.openstreetmap.org">OpenStreetmap</a>.\n
|
||||
Actually, OSMDashboard is a separate app that only can access (temporarily) access the data of track(s) that are going to be shown.
|
||||
|
||||
\n
|
||||
b)
|
||||
Via a "normal" map application (e.g., <a href="http://osmand.net/">OsmAnd</a> and <a href="https://maps.me/">MAPS.ME</a>).
|
||||
Those app need to support the KMZ file format (only locations and timestamps).
|
||||
In fact, an app needs to support <i>ACTION_VIEW</i> with MIME <i>application/vnd.google-earth.kmz</i>.
|
||||
</string>
|
||||
|
||||
<string name="help_storage_pictures_title">How does OpenTracks store pictures that were taken as markers?</string>
|
||||
<string name="help_storage_pictures_content">These are stored directly on the file system.</string>
|
||||
<string name="help_storage_pictures_content">These are stored in the app internal storage (i.e., only accessible within OpenTracks).</string>
|
||||
|
||||
<string name="help_track_export_title">How can I export/share recorded tracks?</string>
|
||||
<string name="help_track_export_content">
|
||||
Tracks can be exported from the Settings as KMZ, KML, or GPX.
|
||||
KMZ contains all data incl. images, while KML contains only the sensor data, and GPX only the location.
|
||||
Tracks shared via the <i>share icon</i> are shared as KMZ and any App can be used to send the data.
|
||||
KMZ/KML all data; GPX only locations.
|
||||
Tracks shared via the <i>share icon</i> are shared as KMZ.
|
||||
</string>
|
||||
<string name="help_data_upload_title">Can OpenTracks upload the data automatically?</string>
|
||||
|
||||
<string name="help_data_google_play_services_title">Does OpenTracks require/use Google Play Service?</string>
|
||||
<string name="help_data_online_title">Does OpenTracks use online services?</string>
|
||||
|
||||
<string name="help_recording_title">How does OpenTracks record data?</string>
|
||||
<string name="help_recording_content">
|
||||
OpenTracks uses the GPS sensor of the device to record the location continuously.
|
||||
If the GPS device reports inaccurate data (e.g., location, speed, elevation), OpenTracks cannot do anything about it.
|
||||
</string>
|
||||
|
||||
<string name="help_data_storage_title">How does OpenTracks store sensor data?</string>
|
||||
<string name="help_data_storage_content">
|
||||
OpenTracks stores sensor data per recorded location.
|
||||
In terms of a heart rate sensor this means that only <i>one</i> heart rate measurement is stored per location.
|
||||
Sensor data is stored per recorded location.
|
||||
In terms of a heart rate sensor, this means that only <i>one</i> heart rate measurement is stored per location.
|
||||
If you are standing still or are indoors, no sensor data will be recorded (but it will be shown).
|
||||
</string>
|
||||
|
||||
<string name="help_gps_troubleshooting_title">OpenTracks is not recording locations?</string>
|
||||
<string name="help_gps_troubleshooting_content">
|
||||
What you can do:\n
|
||||
a) check if GPS is enabled,\n
|
||||
b) improve GPS reception (e.g., go outdoors),\n
|
||||
c) check if with <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a> app, and\n
|
||||
d) disable power saving.\n
|
||||
\n
|
||||
If everything does not help please report a bug.
|
||||
</string>
|
||||
|
||||
<string name="help_support_title">How can I support OpenTracks?</string>
|
||||
<string name="help_support_content">
|
||||
You can:\n
|
||||
a) spread the word about OpenTracks,\n
|
||||
b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">propose features</a>,\n
|
||||
c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues">report bugs</a>,\n
|
||||
d) help in software development,\n
|
||||
e) buy in <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">Google Play Store</a>,\n
|
||||
f) donate, and\n
|
||||
g) do whatever you think helps OpenTracks.
|
||||
</string>
|
||||
|
||||
<!-- Activity Type -->
|
||||
<string name="activity_type_airplane">airplane</string>
|
||||
<string name="activity_type_atv">ATV</string>
|
||||
@@ -167,6 +201,8 @@ At the moment, the recorded locations as well as the timestamps are send to an A
|
||||
<!-- Export -->
|
||||
<string name="export_all_option">All as %1$s</string>
|
||||
<string name="export_all_title">Export to external storage</string>
|
||||
<string name="export_with_photos">with photos</string>
|
||||
<string name="export_without_photos">without photos</string>
|
||||
<string name="export_external_storage">External storage</string>
|
||||
<string name="export_external_storage_error">Exported %1$d of %2$s to %3$s</string>
|
||||
<string name="export_external_storage_option">as %1$s to %2$s</string>
|
||||
|
||||
Reference in New Issue
Block a user