Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 306522863a | |||
| e54d85e04d | |||
| cb30ad27c3 | |||
| 8685c9cc88 | |||
| a6f03d0f23 | |||
| e3c812b155 | |||
| f149de9ce3 | |||
| 3447dd2569 | |||
| b2b8c7fd54 | |||
| 486bcd0aec | |||
| c8a33201f5 | |||
| 4a37c2cc7e | |||
| d23db5bafd | |||
| ac528f9a10 | |||
| 256eadd49d | |||
| 64e58934a5 | |||
| 92dfadfdba | |||
| cbce70143b | |||
| f4b9e3159e | |||
| 928781391d | |||
| 05cd672464 | |||
| d3109bbc37 | |||
| eabdb7c721 | |||
| ad990e8cd7 | |||
| db3eef0104 | |||
| c4f1ba7962 | |||
| 7451fbd413 | |||
| b6c55de55a | |||
| dd3ee6d99e | |||
| bbb6a7c22c | |||
| dc1b50a7bf | |||
| 21bfb88e0b | |||
| aaab8adce9 |
@@ -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
|
||||
@@ -45,19 +45,16 @@ captures/
|
||||
|
||||
|
||||
# IntelliJ IDEA
|
||||
*.iml
|
||||
*.iws
|
||||
/out/
|
||||
|
||||
# User-specific configurations
|
||||
.idea/.name
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/caches/
|
||||
.idea/compiler.xml
|
||||
.idea/copyright/profiles_settings.xml
|
||||
.idea/dataSources.ids
|
||||
.idea/datasources.xml
|
||||
.idea/dictionaries
|
||||
.idea/dynamic.xml
|
||||
.idea/encodings.xml
|
||||
.idea/jsLibraryMappings.xml
|
||||
@@ -67,10 +64,8 @@ captures/
|
||||
.idea/scopes/scope_settings.xml
|
||||
.idea/shelf/
|
||||
.idea/sqlDataSources.xml
|
||||
.idea/tasks.xml
|
||||
.idea/vcs.xml
|
||||
.idea/uiDesigner.xml
|
||||
.idea/workspace.xml
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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,12 @@
|
||||
v3.4.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Dashboard API: multiple tracks
|
||||
- Updated help
|
||||
|
||||
Bugfix:
|
||||
- Fixed crash on fragment re-creation
|
||||
- When paused: prevent stopping app
|
||||
|
||||
Developer:
|
||||
- Enable testing via CI (Github Actions and TravisCI)
|
||||
@@ -0,0 +1,14 @@
|
||||
v3.5.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Marker: select pictures from gallery
|
||||
- Marker: share picture
|
||||
- Use track name as filename when sharing
|
||||
- Removed elevation min/max (was imprecise)
|
||||
|
||||
Bugfix:
|
||||
- Charts use preferred units and rate
|
||||
|
||||
Developer:
|
||||
- Database: remove unused columns
|
||||
- Database: add indices
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 168 KiB |
@@ -19,7 +19,6 @@ public class TestDataUtil {
|
||||
Track track = new Track();
|
||||
track.setId(id);
|
||||
track.setName("Test: " + id);
|
||||
track.setNumberOfPoints(numPoints);
|
||||
for (int i = 0; i < numPoints; i++) {
|
||||
track.addLocation(createLocation(i));
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
package de.dennisguse.opentracks.content.provider;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
@@ -29,7 +29,6 @@ import org.junit.Test;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.data.WaypointsColumns;
|
||||
import de.dennisguse.opentracks.content.provider.CustomContentProvider.DatabaseHelper;
|
||||
|
||||
/**
|
||||
* Tests {@link CustomContentProvider}.
|
||||
@@ -38,30 +37,15 @@ import de.dennisguse.opentracks.content.provider.CustomContentProvider.DatabaseH
|
||||
*/
|
||||
public class CustomContentProviderTest {
|
||||
|
||||
private static final String DATABASE_NAME = "test.db";
|
||||
|
||||
private SQLiteDatabase db;
|
||||
private CustomContentProvider customContentProvider;
|
||||
private Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
context.deleteDatabase(DATABASE_NAME);
|
||||
db = (new DatabaseHelper(context, DATABASE_NAME)).getWritableDatabase();
|
||||
customContentProvider = new CustomContentProvider() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link CustomContentProvider.DatabaseHelper#onCreate(SQLiteDatabase)}.
|
||||
*/
|
||||
@Test
|
||||
public void testDatabaseHelper_OnCreate() {
|
||||
Assert.assertTrue(hasTable(TracksColumns.TABLE_NAME));
|
||||
Assert.assertTrue(hasTable(TrackPointsColumns.TABLE_NAME));
|
||||
Assert.assertTrue(hasTable(WaypointsColumns.TABLE_NAME));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link CustomContentProvider#onCreate(android.content.Context)}.
|
||||
*/
|
||||
@@ -76,73 +60,12 @@ 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table, containing one column.
|
||||
*
|
||||
* @param table the table name
|
||||
*/
|
||||
private void createTable(String table) {
|
||||
db.execSQL("CREATE TABLE " + table + " (test INTEGER)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a table in database.
|
||||
*
|
||||
* @param table the table name
|
||||
*/
|
||||
private void dropTable(String table) {
|
||||
db.execSQL("Drop TABLE " + table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the table exists.
|
||||
*
|
||||
* @param table the table name
|
||||
*/
|
||||
private boolean hasTable(String table) {
|
||||
try {
|
||||
db.rawQuery("select count(*) from " + table, null);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the column in the table exists.
|
||||
*
|
||||
* @param table the table name
|
||||
* @param column the column name
|
||||
*/
|
||||
private boolean hasColumn(String table, String column) {
|
||||
try {
|
||||
db.execSQL("SElECT count(*) from " + table + " order by " + column);
|
||||
} catch (Exception e) {
|
||||
if (e.getMessage().contains("no such column")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up upgrade.
|
||||
*
|
||||
* @param oldVersion thd old database version
|
||||
*/
|
||||
private void setupUpgrade(int oldVersion) {
|
||||
dropTable(TracksColumns.TABLE_NAME);
|
||||
dropTable(TrackPointsColumns.TABLE_NAME);
|
||||
dropTable(WaypointsColumns.TABLE_NAME);
|
||||
createTable(TracksColumns.TABLE_NAME);
|
||||
createTable(TrackPointsColumns.TABLE_NAME);
|
||||
createTable(WaypointsColumns.TABLE_NAME);
|
||||
|
||||
DatabaseHelper databaseHelper = new DatabaseHelper(context);
|
||||
databaseHelper.onUpgrade(db, oldVersion, CustomContentProvider.DATABASE_VERSION);
|
||||
Assert.assertEquals(WaypointsColumns.CONTENT_ITEMTYPE, customContentProvider.getType(ContentUris.appendId(WaypointsColumns.CONTENT_URI.buildUpon(), 1).build()));
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ public class CustomContentProviderUtilsTest {
|
||||
|
||||
private List<Location> testIterator(long trackId, int numPoints, int batchSize, boolean descending, TrackPointFactory trackPointFactory) {
|
||||
long lastPointId = initializeTrack(trackId, numPoints);
|
||||
((ContentProviderUtils) contentProviderUtils).setDefaultCursorBatchSize(batchSize);
|
||||
contentProviderUtils.setDefaultCursorBatchSize(batchSize);
|
||||
List<Location> locations = new ArrayList<Location>(numPoints);
|
||||
try (TrackPointIterator it = contentProviderUtils.getTrackPointLocationIterator(trackId, -1L, descending, trackPointFactory)) {
|
||||
while (it.hasNext()) {
|
||||
@@ -155,7 +155,6 @@ public class CustomContentProviderUtilsTest {
|
||||
Track track = new Track();
|
||||
track.setId(id);
|
||||
track.setName("Test: " + id);
|
||||
track.setNumberOfPoints(numPoints);
|
||||
contentProviderUtils.insertTrack(track);
|
||||
track = contentProviderUtils.getTrack(id);
|
||||
Assert.assertNotNull(track);
|
||||
@@ -183,7 +182,6 @@ public class CustomContentProviderUtilsTest {
|
||||
}
|
||||
|
||||
Assert.assertTrue(numPoints == 0 || lastPointId > 0);
|
||||
Assert.assertEquals(numPoints, track.getNumberOfPoints());
|
||||
Assert.assertEquals(numPoints, counter);
|
||||
|
||||
return lastPointId;
|
||||
@@ -230,7 +228,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());
|
||||
@@ -239,7 +237,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());
|
||||
@@ -265,7 +263,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());
|
||||
@@ -274,7 +272,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());
|
||||
@@ -616,8 +614,8 @@ public class CustomContentProviderUtilsTest {
|
||||
when(cursorMock.getBlob(index++)).thenReturn(sensor);
|
||||
|
||||
Location location = contentProviderUtils.createTrackPoint(cursorMock);
|
||||
Assert.assertEquals((double) longitude, location.getLongitude(), 0.01);
|
||||
Assert.assertEquals((double) latitude, location.getLatitude(), 0.01);
|
||||
Assert.assertEquals(longitude, location.getLongitude(), 0.01);
|
||||
Assert.assertEquals(latitude, location.getLatitude(), 0.01);
|
||||
Assert.assertEquals(time, location.getTime(), 0.01);
|
||||
Assert.assertEquals(speed, location.getSpeed(), 0.01);
|
||||
}
|
||||
@@ -661,7 +659,7 @@ public class CustomContentProviderUtilsTest {
|
||||
Track track = TestDataUtil.getTrack(trackId, 10);
|
||||
contentProviderUtils.insertTrack(track);
|
||||
|
||||
long[] trackpointIds = new long[track.getNumberOfPoints()];
|
||||
long[] trackpointIds = new long[track.getLocations().size()];
|
||||
for (int i = 0; i < trackpointIds.length; i++) {
|
||||
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
|
||||
}
|
||||
@@ -680,7 +678,7 @@ public class CustomContentProviderUtilsTest {
|
||||
Track track = TestDataUtil.getTrack(trackId, 10);
|
||||
contentProviderUtils.insertTrack(track);
|
||||
|
||||
long[] trackpointIds = new long[track.getNumberOfPoints()];
|
||||
long[] trackpointIds = new long[track.getLocations().size()];
|
||||
for (int i = 0; i < trackpointIds.length; i++) {
|
||||
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
|
||||
}
|
||||
@@ -699,7 +697,7 @@ public class CustomContentProviderUtilsTest {
|
||||
Track track = TestDataUtil.getTrack(trackId, 10);
|
||||
contentProviderUtils.insertTrack(track);
|
||||
|
||||
long[] trackpointIds = new long[track.getNumberOfPoints()];
|
||||
long[] trackpointIds = new long[track.getLocations().size()];
|
||||
for (int i = 0; i < trackpointIds.length; i++) {
|
||||
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
|
||||
}
|
||||
@@ -726,7 +724,7 @@ public class CustomContentProviderUtilsTest {
|
||||
Track track = TestDataUtil.getTrack(trackId, 10);
|
||||
contentProviderUtils.insertTrack(track);
|
||||
|
||||
long[] trackpointIds = new long[track.getNumberOfPoints()];
|
||||
long[] trackpointIds = new long[track.getLocations().size()];
|
||||
for (int i = 0; i < trackpointIds.length; i++) {
|
||||
trackpointIds[i] = ContentUris.parseId(contentProviderUtils.insertTrackPoint(track.getLocations().get(i), track.getId()));
|
||||
}
|
||||
@@ -766,4 +764,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}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package de.dennisguse.opentracks.content.provider;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.data.WaypointsColumns;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class CustomSQLiteOpenHelperTest {
|
||||
|
||||
private static final String TRACKS_CREATE_TABLE_V23 = "CREATE TABLE tracks (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, starttime INTEGER, stoptime INTEGER, numpoints INTEGER, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, mingrade FLOAT, maxgrade FLOAT, icon TEXT)";
|
||||
private static final String TRACKPOINTS_CREATE_TABLE_V23 = "CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT)";
|
||||
private static final String WAYPOINTS_CREATE_TABLE_V23 = "CREATE TABLE waypoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, icon TEXT, trackid INTEGER, type INTEGER, length FLOAT, duration INTEGER, starttime INTEGER, startid INTEGER, stopid INTEGER, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, mingrade FLOAT, maxgrade FLOAT, photoUrl TEXT)";
|
||||
|
||||
private static final String DATABASE_NAME = "test.db";
|
||||
|
||||
private Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
/**
|
||||
* Get the SQL create statements for all tables (ordered by name).
|
||||
*
|
||||
* @return Map(TableName, SQL)
|
||||
*/
|
||||
private static Map<String, String> getTableSQL(SQLiteDatabase db) {
|
||||
HashMap<String, String> tableSQL = new HashMap<>();
|
||||
try (Cursor cursor = db.query("sqlite_master", new String[]{"name", "SQL"}, "name IN ('" + TracksColumns.TABLE_NAME + "', '" + TrackPointsColumns.TABLE_NAME + "', '" + WaypointsColumns.TABLE_NAME + "')", null, null, null, "name")) {
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
tableSQL.put(cursor.getString(0), cursor.getString(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
return tableSQL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQL create statements for all indices (ordered by name).
|
||||
*
|
||||
* @return Map(tableName, SQL)
|
||||
*/
|
||||
public static Map<String, String> getIndexSQL(SQLiteDatabase db) {
|
||||
HashMap<String, String> indexSQL = new HashMap<>();
|
||||
try (Cursor cursor = db.rawQuery("SELECT tbl_name, SQL FROM sqlite_master WHERE type = 'index'", null)) {
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
indexSQL.put(cursor.getString(0), cursor.getString(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
return indexSQL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a SQL create statement is in the schema.
|
||||
*
|
||||
* @param sqlCreate the table name
|
||||
*/
|
||||
private static boolean hasSqlCreate(SQLiteDatabase db, String sqlCreate) {
|
||||
try (Cursor cursor = db.rawQuery("SELECT SQL FROM sqlite_master", null)) {
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
String sql = cursor.getString(0);
|
||||
if (sqlCreate.equals(sql)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
context.deleteDatabase(DATABASE_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreate() {
|
||||
try (SQLiteDatabase db = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getWritableDatabase()) {
|
||||
Assert.assertTrue(hasSqlCreate(db, TracksColumns.CREATE_TABLE));
|
||||
|
||||
Assert.assertTrue(hasSqlCreate(db, TrackPointsColumns.CREATE_TABLE));
|
||||
Assert.assertTrue(hasSqlCreate(db, TrackPointsColumns.CREATE_TABLE_INDEX));
|
||||
|
||||
Assert.assertTrue(hasSqlCreate(db, WaypointsColumns.CREATE_TABLE));
|
||||
Assert.assertTrue(hasSqlCreate(db, WaypointsColumns.CREATE_TABLE_INDEX));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onUpgrade_FromVersion23() {
|
||||
// Manually create database schema with version 23 (base version)
|
||||
{
|
||||
SQLiteDatabase dbBase = new SQLiteOpenHelper(context, DATABASE_NAME, null, 23) {
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
}
|
||||
}.getWritableDatabase();
|
||||
|
||||
dbBase.execSQL(TRACKS_CREATE_TABLE_V23);
|
||||
dbBase.execSQL(TRACKPOINTS_CREATE_TABLE_V23);
|
||||
dbBase.execSQL(WAYPOINTS_CREATE_TABLE_V23);
|
||||
|
||||
dbBase.close();
|
||||
}
|
||||
|
||||
// Open database with SQL upgrade
|
||||
Map<String, String> tableByUpgrade;
|
||||
Map<String, String> indicesByUpgrade;
|
||||
try (SQLiteDatabase dbUpgraded = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase()) {
|
||||
tableByUpgrade = getTableSQL(dbUpgraded);
|
||||
indicesByUpgrade = getIndexSQL(dbUpgraded);
|
||||
}
|
||||
|
||||
// Open database via
|
||||
context.deleteDatabase(DATABASE_NAME);
|
||||
|
||||
Map<String, String> tablesByCreate;
|
||||
Map<String, String> indicesByCreate;
|
||||
try (SQLiteDatabase dbCreated = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase()) {
|
||||
tablesByCreate = getTableSQL(dbCreated);
|
||||
indicesByCreate = getIndexSQL(dbCreated);
|
||||
}
|
||||
|
||||
// then - verify table structure
|
||||
Assert.assertEquals(3, tableByUpgrade.size());
|
||||
Assert.assertEquals(tableByUpgrade.size(), tablesByCreate.size());
|
||||
|
||||
Assert.assertEquals(tablesByCreate.get(TracksColumns.TABLE_NAME), tableByUpgrade.get(TracksColumns.TABLE_NAME));
|
||||
Assert.assertEquals(tablesByCreate.get(TrackPointsColumns.TABLE_NAME), tableByUpgrade.get(TrackPointsColumns.TABLE_NAME));
|
||||
Assert.assertEquals(tablesByCreate.get(WaypointsColumns.TABLE_NAME), tableByUpgrade.get(WaypointsColumns.TABLE_NAME));
|
||||
|
||||
// then - verify custom indices
|
||||
Assert.assertEquals(2, indicesByCreate.size());
|
||||
Assert.assertEquals(indicesByUpgrade.get(TrackPointsColumns.TABLE_NAME), indicesByCreate.get(TrackPointsColumns.TABLE_NAME));
|
||||
Assert.assertEquals(indicesByUpgrade.get(WaypointsColumns.TABLE_NAME), indicesByCreate.get(WaypointsColumns.TABLE_NAME));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onDowngrade_ToVersion23() {
|
||||
// Create most recent database schema
|
||||
new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase().close();
|
||||
|
||||
// Downgrade schema to version 23 (base version)
|
||||
Map<String, String> tablesByDowngrade;
|
||||
Map<String, String> indicesByDowngrade;
|
||||
try (SQLiteDatabase db = new CustomSQLiteOpenHelper(context, DATABASE_NAME, 23).getReadableDatabase()) {
|
||||
tablesByDowngrade = getTableSQL(db);
|
||||
indicesByDowngrade = getIndexSQL(db);
|
||||
}
|
||||
|
||||
// then - verify table structure
|
||||
Assert.assertEquals(TRACKS_CREATE_TABLE_V23, tablesByDowngrade.get("tracks"));
|
||||
Assert.assertEquals(TRACKPOINTS_CREATE_TABLE_V23, tablesByDowngrade.get("trackpoints"));
|
||||
Assert.assertEquals(WAYPOINTS_CREATE_TABLE_V23, tablesByDowngrade.get("waypoints"));
|
||||
|
||||
// then - verify custom indices
|
||||
Assert.assertEquals(0, indicesByDowngrade.size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.dennisguse.opentracks.content.provider;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
|
||||
public class ShareContentProviderTest {
|
||||
|
||||
@Test
|
||||
public void testCreateandParseURI() {
|
||||
long[] trackIds = {1, 3, 5};
|
||||
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "TrackName", TrackFileFormat.KML_ONLY_TRACK);
|
||||
|
||||
Assert.assertArrayEquals(trackIds, ShareContentProvider.parseURI(shareURIandMIME.first));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateURIescapeFilename() {
|
||||
long[] trackIds = {1};
|
||||
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "../../&1=1", TrackFileFormat.KML_ONLY_TRACK);
|
||||
|
||||
Assert.assertEquals(Uri.parse("content://de.dennisguse.opentracks.debug.content/tracks/kml_only_track/1/..%2F..%2F%261%3D1.kml"), shareURIandMIME.first);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.stats.TripStatistics;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class AnnouncementUtilsTest {
|
||||
|
||||
private Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@Test
|
||||
public void getAnnouncement() {
|
||||
TripStatistics stats = new TripStatistics();
|
||||
stats.setTotalDistance(20000);
|
||||
stats.setTotalTime(600000);
|
||||
stats.setMovingTime(300000);
|
||||
stats.setMaxSpeed(100);
|
||||
stats.setTotalElevationGain(6000);
|
||||
|
||||
// when
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, stats);
|
||||
|
||||
// then
|
||||
Assert.assertEquals("OpenTracks total distance 20.00 kilometers in 5 minutes 0 seconds at 240.0 kilometers per hour", announcement);
|
||||
}
|
||||
}
|
||||
@@ -164,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.parseTime(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="3272"
|
||||
android:versionName="v3.3.2"> <!-- For FDroid. -->
|
||||
android:versionCode="3305"
|
||||
android:versionName="v3.5.0"> <!-- For FDroid. -->
|
||||
<!-- Permission to initialize services -->
|
||||
<application
|
||||
android:name=".Startup"
|
||||
@@ -31,7 +31,9 @@ limitations under the License.
|
||||
<!-- Activities -->
|
||||
<activity android:name=".AboutActivity" />
|
||||
|
||||
<activity android:name=".TrackDeleteActivity" />
|
||||
<activity
|
||||
android:name=".TrackDeleteActivity"
|
||||
android:theme="@style/ThemeCustomTransparent" />
|
||||
|
||||
<activity
|
||||
android:name=".HelpActivity"
|
||||
|
||||
@@ -16,18 +16,38 @@
|
||||
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Waypoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
|
||||
/**
|
||||
* An activity to add/edit a marker.
|
||||
@@ -39,16 +59,27 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
public static final String EXTRA_MARKER_ID = "marker_id";
|
||||
|
||||
private static final int CAMERA_REQUEST_CODE = 5;
|
||||
private static final int GALLERY_IMG_REQUEST_CODE = 7;
|
||||
|
||||
private static final String TAG = MarkerEditActivity.class.getSimpleName();
|
||||
private long trackId;
|
||||
private long markerId;
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private Waypoint waypoint;
|
||||
|
||||
private MenuItem insertPhotoMenuItem;
|
||||
private MenuItem insertGalleryImgMenuItem;
|
||||
|
||||
private Uri photoUri;
|
||||
private boolean hasCamera;
|
||||
|
||||
// UI elements
|
||||
private EditText waypointName;
|
||||
private AutoCompleteTextView waypointMarkerType;
|
||||
private EditText waypointDescription;
|
||||
private ImageView waypointPhoto;
|
||||
private ImageView waypointDeletePhotoBtn;
|
||||
private Button done;
|
||||
|
||||
@Override
|
||||
@@ -59,12 +90,27 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L);
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
|
||||
|
||||
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|
||||
|
||||
// Setup UI elements
|
||||
waypointName = findViewById(R.id.marker_edit_waypoint_name);
|
||||
waypointMarkerType = findViewById(R.id.marker_edit_waypoint_marker_type);
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.waypoint_types, android.R.layout.simple_dropdown_item_1line);
|
||||
waypointMarkerType.setAdapter(adapter);
|
||||
waypointDescription = findViewById(R.id.marker_edit_waypoint_description);
|
||||
waypointPhoto = findViewById(R.id.marker_edit_waypoint_photo);
|
||||
|
||||
waypointDeletePhotoBtn = findViewById(R.id.marker_edit_waypoint_photo_delete);
|
||||
waypointDeletePhotoBtn.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (waypoint != null && waypoint.hasPhoto())
|
||||
waypoint.setPhotoUrl(null);
|
||||
waypointPhoto.setImageBitmap(null);
|
||||
photoUri = null;
|
||||
hideAndShowOptions();
|
||||
}
|
||||
});
|
||||
|
||||
Button cancel = findViewById(R.id.marker_edit_cancel);
|
||||
cancel.setOnClickListener(new OnClickListener() {
|
||||
@@ -94,6 +140,64 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
return R.layout.marker_edit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.marker_edit, menu);
|
||||
|
||||
insertPhotoMenuItem = menu.findItem(R.id.marker_edit_insert_photo);
|
||||
insertPhotoMenuItem.setVisible(hasCamera);
|
||||
insertGalleryImgMenuItem = menu.findItem(R.id.marker_edit_insert_gallery_img);
|
||||
hideAndShowOptions();
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.marker_edit_insert_photo:
|
||||
createWaypointWithPicture();
|
||||
return true;
|
||||
case R.id.marker_edit_insert_gallery_img:
|
||||
createWaypointWithGalleryImage();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == CAMERA_REQUEST_CODE) {
|
||||
if (resultCode == RESULT_CANCELED) {
|
||||
Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
} else if (resultCode == RESULT_OK) {
|
||||
setWaypointImageView(photoUri);
|
||||
}
|
||||
} else if (requestCode == GALLERY_IMG_REQUEST_CODE) {
|
||||
if (resultCode == RESULT_CANCELED) {
|
||||
Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
} else if (resultCode == RESULT_OK) {
|
||||
Uri srcUri = data.getData();
|
||||
try (ParcelFileDescriptor parcelFd = getContentResolver().openFileDescriptor(srcUri, "r")) {
|
||||
FileDescriptor srcFd = parcelFd.getFileDescriptor();
|
||||
File dstFile = new File(FileUtils.getImageUrl(this, getTrackId()));
|
||||
FileUtils.copy(srcFd, dstFile);
|
||||
|
||||
photoUri = FileUtils.getUriForFile(this, dstFile);
|
||||
setWaypointImageView(photoUri);
|
||||
} catch(Exception e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
Toast.makeText(this, R.string.marker_add_canceled, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI based on the marker id.
|
||||
*/
|
||||
@@ -133,7 +237,62 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
waypointName.setText(waypoint.getName());
|
||||
waypointMarkerType.setText(waypoint.getCategory());
|
||||
waypointDescription.setText(waypoint.getDescription());
|
||||
if (waypoint.hasPhoto()) {
|
||||
photoUri = waypoint.getPhotoURI();
|
||||
setWaypointImageView(photoUri);
|
||||
}
|
||||
}
|
||||
|
||||
hideAndShowOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks and hide/shows all buttons/options about marker photo options.
|
||||
*
|
||||
* If a photo is set then one's options are shown, otherwise another ones are shown.
|
||||
*/
|
||||
private void hideAndShowOptions() {
|
||||
boolean isPhotoSet = (waypoint != null && waypoint.hasPhoto()) || photoUri != null;
|
||||
if (insertPhotoMenuItem != null && insertGalleryImgMenuItem != null) {
|
||||
insertPhotoMenuItem.setVisible(isPhotoSet ? false : true);
|
||||
insertGalleryImgMenuItem.setVisible(isPhotoSet ? false : true);
|
||||
}
|
||||
waypointDeletePhotoBtn.setVisibility(isPhotoSet ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the trackId. If trackId == -1 then get it from waypoint object.
|
||||
*/
|
||||
private long getTrackId() {
|
||||
return trackId == -1 ? waypoint.getTrackId() : trackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ImageView waypointPhoto with the uri photo.
|
||||
*
|
||||
* @param uri the uri photo.
|
||||
*/
|
||||
private void setWaypointImageView(Uri uri) {
|
||||
try (ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r")) {
|
||||
FileDescriptor fd = pfd.getFileDescriptor();
|
||||
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd);
|
||||
waypointPhoto.setImageBitmap(bitmap);
|
||||
hideAndShowOptions();
|
||||
} catch(IOException e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void createWaypointWithPicture() {
|
||||
Pair<Intent, Uri> intentAndPhotoUri = IntentUtils.createTakePictureIntent(this, getTrackId());
|
||||
photoUri = intentAndPhotoUri.second;
|
||||
startActivityForResult(intentAndPhotoUri.first, CAMERA_REQUEST_CODE);
|
||||
}
|
||||
|
||||
private void createWaypointWithGalleryImage() {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
startActivityForResult(intent, GALLERY_IMG_REQUEST_CODE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,7 +303,7 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
waypointName.getText().toString(),
|
||||
waypointMarkerType.getText().toString(),
|
||||
waypointDescription.getText().toString(),
|
||||
null);
|
||||
photoUri != null ? photoUri.toString() : null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,6 +313,7 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
waypoint.setName(waypointName.getText().toString());
|
||||
waypoint.setCategory(waypointMarkerType.getText().toString());
|
||||
waypoint.setDescription(waypointDescription.getText().toString());
|
||||
waypoint.setPhotoUrl(photoUri != null ? photoUri.toString() : null);
|
||||
|
||||
new ContentProviderUtils(this).updateWaypoint(waypoint);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
|
||||
private static final String[] PROJECTION = new String[]{WaypointsColumns._ID,
|
||||
WaypointsColumns.NAME, WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY,
|
||||
WaypointsColumns.TYPE, WaypointsColumns.TIME, WaypointsColumns.PHOTOURL,
|
||||
WaypointsColumns.TIME, WaypointsColumns.PHOTOURL,
|
||||
WaypointsColumns.LATITUDE, WaypointsColumns.LONGITUDE};
|
||||
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
@@ -179,7 +179,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
new String[]{String.valueOf(track.getId())}, null);
|
||||
} else {
|
||||
return new CursorLoader(MarkerListActivity.this, WaypointsColumns.CONTENT_URI, PROJECTION,
|
||||
WaypointsColumns.STARTTIME + " IS NULL", null, null);
|
||||
null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,26 +16,18 @@
|
||||
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Pair;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
@@ -46,8 +38,7 @@ import de.dennisguse.opentracks.content.TrackDataHub;
|
||||
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.ChartDistanceFragment;
|
||||
import de.dennisguse.opentracks.fragments.ChartTimeFragment;
|
||||
import de.dennisguse.opentracks.fragments.ChartFragment;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||
import de.dennisguse.opentracks.fragments.StatsFragment;
|
||||
@@ -72,16 +63,8 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
private static final String TAG = TrackDetailActivity.class.getSimpleName();
|
||||
|
||||
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
|
||||
private static final String PHOTO_URI_KEY = "photo_uri_key";
|
||||
private static final String HAS_PHOTO_KEY = "has_photo_key";
|
||||
|
||||
private static final int CAMERA_REQUEST_CODE = 5;
|
||||
private static final int EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 6;
|
||||
|
||||
// The following are set in onCreate
|
||||
private boolean hasCamera;
|
||||
private Uri photoUri;
|
||||
private boolean hasPhoto;
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
private SharedPreferences sharedPreferences;
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
@@ -104,10 +87,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
@Override
|
||||
public void run() {
|
||||
trackController.update(isRecording(), recordingTrackPaused);
|
||||
if (hasPhoto && photoUri != null) {
|
||||
hasPhoto = false;
|
||||
long markerId = trackRecordingServiceConnection.addMarker(TrackDetailActivity.this, null, null, null, photoUri.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -150,7 +129,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
};
|
||||
|
||||
private MenuItem insertMarkerMenuItem;
|
||||
private MenuItem insertPhotoMenuItem;
|
||||
private MenuItem markerListMenuItem;
|
||||
private MenuItem shareMenuItem;
|
||||
|
||||
@@ -184,10 +162,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
super.onCreate(savedInstanceState);
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
|
||||
|
||||
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
|
||||
photoUri = savedInstanceState != null ? (Uri) savedInstanceState.getParcelable(PHOTO_URI_KEY) : null;
|
||||
hasPhoto = savedInstanceState != null && savedInstanceState.getBoolean(HAS_PHOTO_KEY, false);
|
||||
|
||||
contentProviderUtils = new ContentProviderUtils(this);
|
||||
handleIntent(getIntent());
|
||||
|
||||
@@ -206,17 +180,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) {
|
||||
@@ -311,23 +283,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt(CURRENT_TAB_TAG_KEY, pager.getCurrentItem());
|
||||
|
||||
if (photoUri != null) {
|
||||
outState.putParcelable(PHOTO_URI_KEY, photoUri);
|
||||
}
|
||||
outState.putBoolean(HAS_PHOTO_KEY, hasPhoto);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == CAMERA_REQUEST_CODE) {
|
||||
if (resultCode == RESULT_CANCELED) {
|
||||
Toast.makeText(this, R.string.marker_add_canceled, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
hasPhoto = resultCode == RESULT_OK;
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -347,8 +302,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
getMenuInflater().inflate(R.menu.track_detail, menu);
|
||||
|
||||
insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker);
|
||||
insertPhotoMenuItem = menu.findItem(R.id.track_detail_insert_photo);
|
||||
insertPhotoMenuItem.setVisible(new Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(getPackageManager()) != null);
|
||||
shareMenuItem = menu.findItem(R.id.track_detail_share);
|
||||
markerListMenuItem = menu.findItem(R.id.track_detail_markers);
|
||||
|
||||
@@ -376,9 +329,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
intent = Intent.createChooser(intent, null);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.track_detail_insert_photo:
|
||||
createWaypointWithPicture();
|
||||
return true;
|
||||
case R.id.track_detail_menu_show_on_map:
|
||||
IntentUtils.showTrackOnMap(this, new long[]{trackId});
|
||||
return true;
|
||||
@@ -404,20 +354,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
|
||||
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
|
||||
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
createWaypointWithPicture();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
|
||||
return trackRecordingServiceConnection;
|
||||
@@ -475,7 +411,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
*/
|
||||
private void updateMenuItems(boolean isPaused) {
|
||||
insertMarkerMenuItem.setVisible(isRecording() && !isPaused);
|
||||
insertPhotoMenuItem.setVisible(hasCamera && isRecording() && !isPaused);
|
||||
shareMenuItem.setVisible(!isRecording());
|
||||
markerListMenuItem.setShowAsAction(isRecording() ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
String title;
|
||||
@@ -488,12 +423,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
|
||||
setTitle(title);
|
||||
}
|
||||
|
||||
private void createWaypointWithPicture() {
|
||||
Pair<Intent, Uri> intentAndPhotoUri = IntentUtils.createTakePictureIntent(this, trackId);
|
||||
photoUri = intentAndPhotoUri.second;
|
||||
startActivityForResult(intentAndPhotoUri.first, CAMERA_REQUEST_CODE);
|
||||
}
|
||||
|
||||
public void chooseActivityType(String category) {
|
||||
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), category);
|
||||
}
|
||||
|
||||
@@ -295,6 +295,10 @@ public class ChartView extends View {
|
||||
series[index].setEnabled(enabled);
|
||||
}
|
||||
|
||||
public boolean getMetricUnits() {
|
||||
return metricUnits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets metric units.
|
||||
*
|
||||
@@ -304,6 +308,10 @@ public class ChartView extends View {
|
||||
metricUnits = value;
|
||||
}
|
||||
|
||||
public boolean getReportSpeed() {
|
||||
return reportSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets report speed.
|
||||
*
|
||||
@@ -486,7 +494,6 @@ public class ChartView extends View {
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
synchronized (chartData) {
|
||||
|
||||
canvas.save();
|
||||
|
||||
canvas.drawColor(Color.WHITE);
|
||||
|
||||
@@ -111,8 +111,7 @@ 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.
|
||||
|
||||
@@ -38,10 +38,6 @@ public class Track {
|
||||
private String description = "";
|
||||
private String category = "";
|
||||
|
||||
/*
|
||||
* The number of location points (present even if the points themselves are not loaded)
|
||||
*/
|
||||
private int numberOfPoints = 0;
|
||||
private String icon = "";
|
||||
|
||||
private TripStatistics tripStatistics = new TripStatistics();
|
||||
@@ -84,14 +80,6 @@ public class Track {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public int getNumberOfPoints() {
|
||||
return numberOfPoints;
|
||||
}
|
||||
|
||||
public void setNumberOfPoints(int numberOfPoints) {
|
||||
this.numberOfPoints = numberOfPoints;
|
||||
}
|
||||
|
||||
public String getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
@@ -29,15 +29,20 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
public interface TrackPointsColumns extends BaseColumns {
|
||||
|
||||
String TABLE_NAME = "trackpoints";
|
||||
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
|
||||
Uri CONTENT_URI_BY_ID = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
|
||||
Uri CONTENT_URI_BY_TRACKID = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/trackid");
|
||||
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint";
|
||||
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.trackpoint";
|
||||
String DEFAULT_SORT_ORDER = "_id";
|
||||
|
||||
// Columns
|
||||
String TRACKID = "trackid";
|
||||
|
||||
String LONGITUDE = "longitude";
|
||||
String LATITUDE = "latitude";
|
||||
double PAUSE_LATITUDE = 100.0;
|
||||
double RESUME_LATITUDE = 200.0;
|
||||
|
||||
String TIME = "time";
|
||||
String ALTITUDE = "elevation";
|
||||
String ACCURACY = "accuracy";
|
||||
@@ -59,5 +64,7 @@ public interface TrackPointsColumns extends BaseColumns {
|
||||
+ BEARING + " FLOAT, "
|
||||
+ SENSOR_HEARTRATE + " FLOAT, "
|
||||
+ SENSOR_CADENCE + " FLOAT, "
|
||||
+ SENSOR_POWER + " FLOAT);";
|
||||
+ SENSOR_POWER + " FLOAT)";
|
||||
|
||||
String CREATE_TABLE_INDEX = "CREATE INDEX " + TABLE_NAME + "_" + TRACKID + "_index ON " + TABLE_NAME + "(" + TRACKID + ")";
|
||||
}
|
||||
|
||||
@@ -38,34 +38,20 @@ public interface TracksColumns extends BaseColumns {
|
||||
String NAME = "name"; // track name
|
||||
String DESCRIPTION = "description"; // track description
|
||||
String CATEGORY = "category"; // track activity type
|
||||
@Deprecated
|
||||
String STARTID = "startid"; // first track point id // TODO UNUSED
|
||||
@Deprecated
|
||||
String STOPID = "stopid"; // last track point id // TODO UNUSED
|
||||
String STARTTIME = "starttime"; // track start time
|
||||
String STOPTIME = "stoptime"; // track stop time
|
||||
String NUMPOINTS = "numpoints"; // number of track points
|
||||
@Deprecated
|
||||
String NUMPOINTS = "numpoints"; // number of track points //TODO UNUSED
|
||||
String TOTALDISTANCE = "totaldistance"; // total distance
|
||||
String TOTALTIME = "totaltime"; // total time
|
||||
String MOVINGTIME = "movingtime"; // moving time
|
||||
|
||||
@Deprecated
|
||||
String MINLAT = "minlat"; // minimum latitude // TODO UNUSED
|
||||
@Deprecated
|
||||
String MAXLAT = "maxlat"; // maximum latitude // TODO UNUSED
|
||||
@Deprecated
|
||||
String MINLON = "minlon"; // minimum longitude //TODO UNUSED
|
||||
@Deprecated
|
||||
String MAXLON = "maxlon"; // maximum longitude //TODO UNUSED
|
||||
|
||||
String AVGSPEED = "avgspeed"; // average speed
|
||||
String AVGMOVINGSPEED = "avgmovingspeed"; // average moving speed
|
||||
String MAXSPEED = "maxspeed"; // maximum speed
|
||||
String MINELEVATION = "minelevation"; // minimum elevation
|
||||
String MAXELEVATION = "maxelevation"; // maximum elevation
|
||||
String ELEVATIONGAIN = "elevationgain"; // elevation gain
|
||||
String MINGRADE = "mingrade"; // minimum grade
|
||||
String MAXGRADE = "maxgrade"; // maximum grade
|
||||
String ICON = "icon"; // track activity type icon
|
||||
|
||||
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" // table
|
||||
@@ -73,25 +59,17 @@ public interface TracksColumns extends BaseColumns {
|
||||
+ NAME + " TEXT, " // name
|
||||
+ DESCRIPTION + " TEXT, " // description
|
||||
+ CATEGORY + " TEXT, " // category
|
||||
+ STARTID + " INTEGER, " // start id
|
||||
+ STOPID + " INTEGER, " // stop id
|
||||
+ STARTTIME + " INTEGER, " // start time
|
||||
+ STOPTIME + " INTEGER, " // stop time
|
||||
+ NUMPOINTS + " INTEGER, " // num points
|
||||
+ TOTALDISTANCE + " FLOAT, " // total distance
|
||||
+ TOTALTIME + " INTEGER, " // total time
|
||||
+ MOVINGTIME + " INTEGER, " // moving time
|
||||
+ MINLAT + " INTEGER, " // min latitude
|
||||
+ MAXLAT + " INTEGER, " // max latitude
|
||||
+ MINLON + " INTEGER, " // min longitude
|
||||
+ MAXLON + " INTEGER, " // max longitude
|
||||
+ AVGSPEED + " FLOAT, " // average speed
|
||||
+ AVGMOVINGSPEED + " FLOAT, " // average moving speed
|
||||
+ MAXSPEED + " FLOAT, " // max speed
|
||||
+ MINELEVATION + " FLOAT, " // min elevation
|
||||
+ MAXELEVATION + " FLOAT, " // max elevation
|
||||
+ ELEVATIONGAIN + " FLOAT, " // elevation gain
|
||||
+ MINGRADE + " FLOAT, " // min grade
|
||||
+ MAXGRADE + " FLOAT, " // max grade
|
||||
+ ICON + " TEXT);"; // icon
|
||||
+ ICON + " TEXT)"; // icon
|
||||
}
|
||||
|
||||
@@ -40,49 +40,17 @@ public interface WaypointsColumns extends BaseColumns {
|
||||
String CATEGORY = "category"; // waypoint category
|
||||
String ICON = "icon"; // waypoint icon
|
||||
String TRACKID = "trackid"; // track id
|
||||
@Deprecated
|
||||
String TYPE = "type"; // type // TODO UNUSED
|
||||
|
||||
String LENGTH = "length"; // length of the track (without smoothing)
|
||||
String DURATION = "duration"; // total duration of the track (not from last waypoint)
|
||||
String STARTTIME = "starttime"; // start time of the trip statistics
|
||||
@Deprecated
|
||||
String STARTID = "startid"; // start track point id // TODO UNUSED
|
||||
@Deprecated
|
||||
String STOPID = "stopid"; // stop track point id // TODO UNUSED
|
||||
String DURATION = "duration"; // total duration of the track from the beginning until now
|
||||
|
||||
String LONGITUDE = "longitude"; // longitude
|
||||
String LATITUDE = "latitude"; // latitude
|
||||
String TIME = "time"; // time
|
||||
String ALTITUDE = "elevation"; // altitude
|
||||
String ACCURACY = "accuracy"; // accuracy
|
||||
String SPEED = "speed"; // speed
|
||||
String BEARING = "bearing"; // bearing
|
||||
|
||||
@Deprecated
|
||||
String TOTALDISTANCE = "totaldistance"; // total distance
|
||||
@Deprecated
|
||||
String TOTALTIME = "totaltime"; // total time
|
||||
@Deprecated
|
||||
String MOVINGTIME = "movingtime"; // moving time
|
||||
@Deprecated
|
||||
String AVGSPEED = "avgspeed"; // average speed
|
||||
|
||||
@Deprecated
|
||||
String AVGMOVINGSPEED = "avgmovingspeed"; // average moving speed // TODO UNUSED
|
||||
@Deprecated
|
||||
String MAXSPEED = "maxspeed"; // max speed // TODO UNUSED
|
||||
@Deprecated
|
||||
String MINELEVATION = "minelevation"; // min elevation // TODO UNUSED
|
||||
@Deprecated
|
||||
String MAXELEVATION = "maxelevation"; // max elevation // TODO UNUSED
|
||||
@Deprecated
|
||||
String ELEVATIONGAIN = "elevationgain"; // elevation gain // TODO UNUSED
|
||||
@Deprecated
|
||||
String MINGRADE = "mingrade"; // min grade // TODO UNUSED
|
||||
@Deprecated
|
||||
String MAXGRADE = "maxgrade"; // max grade // TODO UNUSED
|
||||
|
||||
String PHOTOURL = "photoUrl"; // url for the photo
|
||||
|
||||
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
@@ -92,30 +60,16 @@ public interface WaypointsColumns extends BaseColumns {
|
||||
+ CATEGORY + " TEXT, "
|
||||
+ ICON + " TEXT, "
|
||||
+ TRACKID + " INTEGER, "
|
||||
+ TYPE + " INTEGER, "
|
||||
+ LENGTH + " FLOAT, "
|
||||
+ DURATION + " INTEGER, "
|
||||
+ STARTTIME + " INTEGER, "
|
||||
+ STARTID + " INTEGER, "
|
||||
+ STOPID + " INTEGER, "
|
||||
+ LONGITUDE + " INTEGER, "
|
||||
+ LATITUDE + " INTEGER, "
|
||||
+ TIME + " INTEGER, "
|
||||
+ ALTITUDE + " FLOAT, "
|
||||
+ ACCURACY + " FLOAT, "
|
||||
+ SPEED + " FLOAT, "
|
||||
+ BEARING + " FLOAT, "
|
||||
+ TOTALDISTANCE + " FLOAT, "
|
||||
+ TOTALTIME + " INTEGER, "
|
||||
+ MOVINGTIME + " INTEGER, "
|
||||
+ AVGSPEED + " FLOAT, "
|
||||
+ AVGMOVINGSPEED + " FLOAT, "
|
||||
+ MAXSPEED + " FLOAT, "
|
||||
+ MINELEVATION + " FLOAT, "
|
||||
+ MAXELEVATION + " FLOAT, "
|
||||
+ ELEVATIONGAIN + " FLOAT, "
|
||||
+ MINGRADE + " FLOAT, "
|
||||
+ MAXGRADE + " FLOAT, "
|
||||
+ PHOTOURL + " TEXT"
|
||||
+ ");";
|
||||
+ ")";
|
||||
|
||||
String CREATE_TABLE_INDEX = "CREATE INDEX " + TABLE_NAME + "_" + TRACKID + "_index ON " + TABLE_NAME + "(" + TRACKID + ")";
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
@@ -68,6 +69,7 @@ public class ContentProviderUtils {
|
||||
* Maximum number of waypoints that will be loaded at one time.
|
||||
*/
|
||||
public static final int MAX_LOADED_WAYPOINTS_POINTS = 10000;
|
||||
public static final String ID_SEPARATOR = ",";
|
||||
|
||||
private final IContentResolver contentResolver;
|
||||
private int defaultCursorBatchSize = 2000;
|
||||
@@ -109,7 +111,6 @@ public class ContentProviderUtils {
|
||||
int categoryIndex = cursor.getColumnIndexOrThrow(TracksColumns.CATEGORY);
|
||||
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
|
||||
int stopTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STOPTIME);
|
||||
int numPointsIndex = cursor.getColumnIndexOrThrow(TracksColumns.NUMPOINTS);
|
||||
int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
|
||||
int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
|
||||
int movingTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.MOVINGTIME);
|
||||
@@ -117,8 +118,6 @@ public class ContentProviderUtils {
|
||||
int minElevationIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINELEVATION);
|
||||
int maxElevationIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXELEVATION);
|
||||
int elevationGainIndex = cursor.getColumnIndexOrThrow(TracksColumns.ELEVATIONGAIN);
|
||||
int minGradeIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINGRADE);
|
||||
int maxGradeIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXGRADE);
|
||||
int iconIndex = cursor.getColumnIndexOrThrow(TracksColumns.ICON);
|
||||
|
||||
Track track = new Track();
|
||||
@@ -141,9 +140,6 @@ public class ContentProviderUtils {
|
||||
if (!cursor.isNull(stopTimeIndex)) {
|
||||
tripStatistics.setStopTime(cursor.getLong(stopTimeIndex));
|
||||
}
|
||||
if (!cursor.isNull(numPointsIndex)) {
|
||||
track.setNumberOfPoints(cursor.getInt(numPointsIndex));
|
||||
}
|
||||
if (!cursor.isNull(totalDistanceIndex)) {
|
||||
tripStatistics.setTotalDistance(cursor.getFloat(totalDistanceIndex));
|
||||
}
|
||||
@@ -165,12 +161,6 @@ public class ContentProviderUtils {
|
||||
if (!cursor.isNull(elevationGainIndex)) {
|
||||
tripStatistics.setTotalElevationGain(cursor.getFloat(elevationGainIndex));
|
||||
}
|
||||
if (!cursor.isNull(minGradeIndex)) {
|
||||
tripStatistics.setMinGrade(cursor.getFloat(minGradeIndex));
|
||||
}
|
||||
if (!cursor.isNull(maxGradeIndex)) {
|
||||
tripStatistics.setMaxGrade(cursor.getFloat(maxGradeIndex));
|
||||
}
|
||||
if (!cursor.isNull(iconIndex)) {
|
||||
track.setIcon(cursor.getString(iconIndex));
|
||||
}
|
||||
@@ -181,13 +171,13 @@ public class ContentProviderUtils {
|
||||
* Deletes all tracks (including waypoints and track points).
|
||||
*/
|
||||
public void deleteAllTracks(Context context) {
|
||||
contentResolver.delete(TrackPointsColumns.CONTENT_URI, null, null);
|
||||
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, null, null);
|
||||
contentResolver.delete(WaypointsColumns.CONTENT_URI, null, null);
|
||||
// Delete tracks last since it triggers a database vaccum call
|
||||
contentResolver.delete(TracksColumns.CONTENT_URI, null, null);
|
||||
|
||||
File dir = FileUtils.getPhotoDir(context);
|
||||
deleteDirectoryRecurse(context, dir);
|
||||
deleteDirectoryRecurse(dir);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,18 +195,17 @@ public class ContentProviderUtils {
|
||||
|
||||
/**
|
||||
* Deletes track points and waypoints of a track.
|
||||
* Assumes {@link TracksColumns#NUMPOINTS} will be updated by the caller.
|
||||
*
|
||||
* @param trackId the track id
|
||||
*/
|
||||
private void deleteTrackPointsAndWaypoints(Context context, long trackId) {
|
||||
String where = TrackPointsColumns.TRACKID + "=?";
|
||||
String[] selectionArgs = new String[]{Long.toString(trackId)};
|
||||
contentResolver.delete(TrackPointsColumns.CONTENT_URI, where, selectionArgs);
|
||||
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, where, selectionArgs);
|
||||
|
||||
contentResolver.delete(WaypointsColumns.CONTENT_URI, WaypointsColumns.TRACKID + "=?",
|
||||
new String[]{Long.toString(trackId)});
|
||||
deleteDirectoryRecurse(context, FileUtils.getPhotoDir(context, trackId));
|
||||
deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,10 +213,10 @@ public class ContentProviderUtils {
|
||||
*
|
||||
* @param dir the directory
|
||||
*/
|
||||
private void deleteDirectoryRecurse(Context context, File dir) {
|
||||
if (dir.exists() && dir.isDirectory()) {
|
||||
private void deleteDirectoryRecurse(File dir) {
|
||||
if (dir != null && dir.exists() && dir.isDirectory()) {
|
||||
for (File child : dir.listFiles()) {
|
||||
deleteDirectoryRecurse(context, child);
|
||||
deleteDirectoryRecurse(child);
|
||||
}
|
||||
dir.delete();
|
||||
}
|
||||
@@ -330,7 +319,6 @@ public class ContentProviderUtils {
|
||||
values.put(TracksColumns.CATEGORY, track.getCategory());
|
||||
values.put(TracksColumns.STARTTIME, tripStatistics.getStartTime());
|
||||
values.put(TracksColumns.STOPTIME, tripStatistics.getStopTime());
|
||||
values.put(TracksColumns.NUMPOINTS, track.getNumberOfPoints());
|
||||
values.put(TracksColumns.TOTALDISTANCE, tripStatistics.getTotalDistance());
|
||||
values.put(TracksColumns.TOTALTIME, tripStatistics.getTotalTime());
|
||||
values.put(TracksColumns.MOVINGTIME, tripStatistics.getMovingTime());
|
||||
@@ -340,8 +328,6 @@ public class ContentProviderUtils {
|
||||
values.put(TracksColumns.MINELEVATION, tripStatistics.getMinElevation());
|
||||
values.put(TracksColumns.MAXELEVATION, tripStatistics.getMaxElevation());
|
||||
values.put(TracksColumns.ELEVATIONGAIN, tripStatistics.getTotalElevationGain());
|
||||
values.put(TracksColumns.MINGRADE, tripStatistics.getMinGrade());
|
||||
values.put(TracksColumns.MAXGRADE, tripStatistics.getMaxGrade());
|
||||
values.put(TracksColumns.ICON, track.getIcon());
|
||||
|
||||
return values;
|
||||
@@ -367,7 +353,6 @@ public class ContentProviderUtils {
|
||||
int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
|
||||
int altitudeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.ALTITUDE);
|
||||
int accuracyIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.ACCURACY);
|
||||
int speedIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.SPEED);
|
||||
int bearingIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.BEARING);
|
||||
int photoUrlIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.PHOTOURL);
|
||||
|
||||
@@ -412,9 +397,6 @@ public class ContentProviderUtils {
|
||||
if (!cursor.isNull(accuracyIndex)) {
|
||||
location.setAccuracy(cursor.getFloat(accuracyIndex));
|
||||
}
|
||||
if (!cursor.isNull(speedIndex)) {
|
||||
location.setSpeed(cursor.getFloat(speedIndex));
|
||||
}
|
||||
if (!cursor.isNull(bearingIndex)) {
|
||||
location.setBearing(cursor.getFloat(bearingIndex));
|
||||
}
|
||||
@@ -611,9 +593,6 @@ public class ContentProviderUtils {
|
||||
if (location.hasAccuracy()) {
|
||||
values.put(WaypointsColumns.ACCURACY, location.getAccuracy());
|
||||
}
|
||||
if (location.hasSpeed()) {
|
||||
values.put(WaypointsColumns.SPEED, location.getSpeed());
|
||||
}
|
||||
if (location.hasBearing()) {
|
||||
values.put(WaypointsColumns.BEARING, location.getBearing());
|
||||
}
|
||||
@@ -659,7 +638,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -846,7 +825,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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -926,7 +905,7 @@ 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() {
|
||||
@@ -942,4 +921,26 @@ public class ContentProviderUtils {
|
||||
void setDefaultCursorBatchSize(int defaultCursorBatchSize) {
|
||||
this.defaultCursorBatchSize = defaultCursorBatchSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an array of IDs as comma separated string value
|
||||
*
|
||||
* @param ids array with IDs
|
||||
* @return comma separated list of ids
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -200,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);
|
||||
@@ -212,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);
|
||||
@@ -220,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);
|
||||
@@ -242,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 + ")";
|
||||
}
|
||||
@@ -253,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 + ")";
|
||||
}
|
||||
@@ -264,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 + ")";
|
||||
}
|
||||
@@ -289,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -328,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);
|
||||
}
|
||||
@@ -380,31 +384,4 @@ public abstract class CustomContentProvider extends ContentProvider {
|
||||
WAYPOINTS,
|
||||
WAYPOINTS_BY_ID
|
||||
}
|
||||
|
||||
/**
|
||||
* Database helper for creating and upgrading the database.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static class DatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
public DatabaseHelper(Context context) {
|
||||
this(context, DATABASE_NAME);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public DatabaseHelper(Context context, String databaseName) {
|
||||
super(context, databaseName, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(TrackPointsColumns.CREATE_TABLE);
|
||||
db.execSQL(TracksColumns.CREATE_TABLE);
|
||||
db.execSQL(WaypointsColumns.CREATE_TABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package de.dennisguse.opentracks.content.provider;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.data.WaypointsColumns;
|
||||
|
||||
/**
|
||||
* Database helper for creating and upgrading the database.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
@VisibleForTesting
|
||||
static final int DATABASE_VERSION = 25;
|
||||
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public CustomSQLiteOpenHelper(Context context, String databaseName, int databaseVersion) {
|
||||
super(context, databaseName, null, databaseVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(TrackPointsColumns.CREATE_TABLE);
|
||||
db.execSQL(TrackPointsColumns.CREATE_TABLE_INDEX);
|
||||
|
||||
db.execSQL(TracksColumns.CREATE_TABLE);
|
||||
|
||||
db.execSQL(WaypointsColumns.CREATE_TABLE);
|
||||
db.execSQL(WaypointsColumns.CREATE_TABLE_INDEX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
for (int toVersion = oldVersion + 1; toVersion <= newVersion; toVersion++) {
|
||||
Log.i(TAG, "Upgrade from " + oldVersion + " to " + toVersion);
|
||||
switch (toVersion) {
|
||||
case 24:
|
||||
upgradeFrom23to24(db);
|
||||
break;
|
||||
case 25:
|
||||
upgradeFrom24to25(db);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
for (int toVersion = oldVersion - 1; toVersion >= newVersion; toVersion--) {
|
||||
Log.i(TAG, "Downgrade from " + oldVersion + " to " + toVersion);
|
||||
switch (toVersion) {
|
||||
case 23:
|
||||
downgradeFrom24to23(db);
|
||||
break;
|
||||
case 24:
|
||||
downgradeFrom25to24(db);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade from database version 23 (waypoints, tracks): remove unused columns.
|
||||
* SQLite3 does not support drop columns; therefore new tables are created and data is copied.
|
||||
*/
|
||||
private void upgradeFrom23to24(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
db.execSQL("ALTER TABLE tracks RENAME TO tracks_old");
|
||||
db.execSQL("CREATE TABLE tracks (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, starttime INTEGER, stoptime INTEGER, numpoints INTEGER, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, icon TEXT)");
|
||||
db.execSQL("INSERT INTO tracks SELECT _id, name, description, category, starttime, stoptime, numpoints, totaldistance, totaltime, movingtime, avgspeed, avgmovingspeed, maxspeed, minelevation, maxelevation, elevationgain, icon FROM tracks_old");
|
||||
db.execSQL("DROP TABLE tracks_old");
|
||||
|
||||
db.execSQL("ALTER TABLE waypoints RENAME TO waypoints_old");
|
||||
db.execSQL("CREATE TABLE waypoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, icon TEXT, trackid INTEGER, length FLOAT, duration INTEGER, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, bearing FLOAT, photoUrl TEXT)");
|
||||
db.execSQL("INSERT INTO waypoints SELECT _id, name, description, category, icon, trackid, length, duration, longitude, latitude, time, elevation, accuracy, bearing, photoUrl FROM waypoints_old");
|
||||
db.execSQL("DROP TABLE waypoints_old");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
private void downgradeFrom24to23(SQLiteDatabase db) {
|
||||
//Not needed as the delete columns did not contain any data
|
||||
db.beginTransaction();
|
||||
db.execSQL("ALTER TABLE tracks RENAME TO tracks_old");
|
||||
db.execSQL("CREATE TABLE tracks (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, starttime INTEGER, stoptime INTEGER, numpoints INTEGER, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, mingrade FLOAT, maxgrade FLOAT, icon TEXT)");
|
||||
db.execSQL("INSERT INTO tracks SELECT _id, name, description, category, starttime, stoptime, numpoints, totaldistance, totaltime, movingtime, avgspeed, avgmovingspeed, maxspeed, minelevation, maxelevation, elevationgain, 0, 0, icon FROM tracks_old");
|
||||
db.execSQL("DROP TABLE tracks_old");
|
||||
|
||||
db.execSQL("ALTER TABLE waypoints RENAME TO waypoints_old");
|
||||
db.execSQL("CREATE TABLE waypoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, icon TEXT, trackid INTEGER, type INTEGER, length FLOAT, duration INTEGER, starttime INTEGER, startid INTEGER, stopid INTEGER, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, mingrade FLOAT, maxgrade FLOAT, photoUrl TEXT)");
|
||||
db.execSQL("INSERT INTO waypoints SELECT _id, name, description, category, icon, trackid, 0, length, duration, 0, 0, 0, longitude, latitude, time, elevation, accuracy, 0, bearing, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, photoUrl FROM waypoints_old");
|
||||
db.execSQL("DROP TABLE waypoints_old");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
private void upgradeFrom24to25(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
|
||||
db.execSQL("CREATE INDEX waypoints_trackid_index ON waypoints(trackid)");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
private void downgradeFrom25to24(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
db.execSQL("DROP INDEX trackpoints_trackid_index");
|
||||
db.execSQL("DROP INDEX waypoints_trackid_index");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.android.IContentResolver;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
@@ -58,30 +59,30 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
|
||||
private static final String TRACKID_DELIMITER = "_";
|
||||
|
||||
static {
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.GPX.getName() + "/*", URI_GPX);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.GPX.getName() + "/*/*", URI_GPX);
|
||||
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_ONLY_TRACK.getName() + "/*", URI_KML_ONLY);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_WITH_TRACKDETAIL.getName() + "/*", URI_KML_WITH_TRACKDETAIL);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*", URI_KML_WITH_TRACKDETAIL_SENSORDATA);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_ONLY_TRACK.getName() + "/*", URI_KMZ_ONLY_TRACK);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_ONLY_TRACK.getName() + "/*/*", URI_KML_ONLY);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_WITH_TRACKDETAIL.getName() + "/*/*", URI_KML_WITH_TRACKDETAIL);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*/*", URI_KML_WITH_TRACKDETAIL_SENSORDATA);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_ONLY_TRACK.getName() + "/*/*", URI_KMZ_ONLY_TRACK);
|
||||
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL.getName() + "/*", URI_KMZ_WITH_TRACKDETAIL);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*", URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getName() + "/*", URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES);
|
||||
}
|
||||
|
||||
public static Pair<Uri, String> createURI(long[] trackIds, @NonNull TrackFileFormat trackFileFormat) {
|
||||
public static Pair<Uri, String> createURI(long[] trackIds, String trackName, @NonNull TrackFileFormat trackFileFormat) {
|
||||
if (trackIds.length == 0) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
StringBuilder trackIdBuilder = new StringBuilder();
|
||||
for (long trackId : trackIds) {
|
||||
builder.append(trackId).append(TRACKID_DELIMITER);
|
||||
trackIdBuilder.append(trackId).append(TRACKID_DELIMITER);
|
||||
}
|
||||
builder.deleteCharAt(builder.lastIndexOf(TRACKID_DELIMITER));
|
||||
trackIdBuilder.deleteCharAt(trackIdBuilder.lastIndexOf(TRACKID_DELIMITER));
|
||||
|
||||
Uri uri = Uri.parse(TracksColumns.CONTENT_URI + "/" + trackFileFormat.getName() + "/" + builder + "." + trackFileFormat.getExtension());
|
||||
Uri uri = Uri.parse(TracksColumns.CONTENT_URI + "/" + trackFileFormat.getName() + "/" + trackIdBuilder + "/" + Uri.encode(trackName) + "." + trackFileFormat.getExtension());
|
||||
String mime = getTypeMime(uri);
|
||||
|
||||
Log.d(TAG, "Created uri " + uri.toString() + " with MIME " + mime);
|
||||
@@ -89,18 +90,18 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
|
||||
return new Pair<>(uri, mime);
|
||||
}
|
||||
|
||||
private static long[] parseURI(Uri uri) {
|
||||
String lastPathSegment = uri.getLastPathSegment();
|
||||
if (lastPathSegment == null) {
|
||||
protected static long[] parseURI(Uri uri) {
|
||||
List<String> uriPaths = uri.getPathSegments();
|
||||
if (uriPaths == null || uriPaths.size() < 2) {
|
||||
Log.d(TAG, "URI does not contain any trackIds.");
|
||||
return new long[]{};
|
||||
}
|
||||
|
||||
String fileExtension = "." + getTrackFileFormat(uri).getExtension();
|
||||
String[] lastPathSegmentSplit = lastPathSegment.replace(fileExtension, "").split(TRACKID_DELIMITER);
|
||||
String[] uriTrackIds = uriPaths.get(2).split(TRACKID_DELIMITER);
|
||||
|
||||
long[] trackIds = new long[lastPathSegmentSplit.length];
|
||||
long[] trackIds = new long[uriTrackIds.length];
|
||||
for (int i = 0; i < trackIds.length; i++) {
|
||||
trackIds[i] = Long.valueOf(lastPathSegmentSplit[i]);
|
||||
trackIds[i] = Long.valueOf(uriTrackIds[i]);
|
||||
}
|
||||
return trackIds;
|
||||
}
|
||||
|
||||
@@ -108,4 +108,13 @@ public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
if (cursor == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return cursor.getCount();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
public class ChartDistanceFragment extends ChartFragment {
|
||||
|
||||
public ChartDistanceFragment() {
|
||||
super(true);
|
||||
}
|
||||
}
|
||||
@@ -25,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;
|
||||
@@ -56,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<>();
|
||||
|
||||
@@ -68,8 +76,6 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
private TripStatisticsUpdater tripStatisticsUpdater;
|
||||
private long startTime;
|
||||
|
||||
private boolean metricUnits = true;
|
||||
private boolean reportSpeed = true;
|
||||
private int recordingDistanceInterval;
|
||||
|
||||
// Modes of operation
|
||||
@@ -82,13 +88,9 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
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;
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
|
||||
if (metricUnits != chartView.getMetricUnits()) {
|
||||
chartView.setMetricUnits(metricUnits);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
@@ -101,9 +103,8 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
}
|
||||
}
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||
boolean speed = PreferencesUtils.isReportSpeed(getContext());
|
||||
if (reportSpeed != speed) {
|
||||
reportSpeed = speed;
|
||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext());
|
||||
if (reportSpeed != chartView.getReportSpeed()) {
|
||||
chartView.setReportSpeed(reportSpeed);
|
||||
setSeriesEnabled(ChartView.SPEED_SERIES, reportSpeed);
|
||||
setSeriesEnabled(ChartView.PACE_SERIES, !reportSpeed);
|
||||
@@ -139,14 +140,13 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
}
|
||||
};
|
||||
|
||||
public ChartFragment(boolean chartByDistance) {
|
||||
this.chartByDistance = chartByDistance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
chartByDistance = getArguments().getBoolean(KEY_CHART_VIEW_BY_DISTANCE_KEY, true);
|
||||
|
||||
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceIntervalDefault(getContext());
|
||||
|
||||
// Create a chartView here to store data thus won't need to reload all the data on every onStart or onResume.
|
||||
@@ -171,6 +171,7 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
super.onResume();
|
||||
resumeTrackDataHub();
|
||||
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
|
||||
|
||||
checkChartSettings();
|
||||
getActivity().runOnUiThread(updateChart);
|
||||
@@ -180,7 +181,7 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
pauseTrackDataHub();
|
||||
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
|
||||
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -190,20 +191,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()) {
|
||||
@@ -284,10 +271,10 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
private void checkChartSettings() {
|
||||
boolean needUpdate = false;
|
||||
|
||||
if (setSeriesEnabled(ChartView.SPEED_SERIES, reportSpeed)) {
|
||||
if (setSeriesEnabled(ChartView.SPEED_SERIES, chartView.getReportSpeed())) {
|
||||
needUpdate = true;
|
||||
}
|
||||
if (setSeriesEnabled(ChartView.PACE_SERIES, !reportSpeed)) {
|
||||
if (setSeriesEnabled(ChartView.PACE_SERIES, !chartView.getReportSpeed())) {
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
@@ -388,7 +375,7 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
TripStatistics tripStatistics = tripStatisticsUpdater.getTripStatistics();
|
||||
if (chartByDistance) {
|
||||
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
|
||||
if (!metricUnits) {
|
||||
if (!chartView.getMetricUnits()) {
|
||||
distance *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
timeOrDistance = distance;
|
||||
@@ -397,12 +384,12 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
}
|
||||
|
||||
elevation = tripStatisticsUpdater.getSmoothedElevation();
|
||||
if (!metricUnits) {
|
||||
if (!chartView.getMetricUnits()) {
|
||||
elevation *= UnitConversions.M_TO_FT;
|
||||
}
|
||||
|
||||
speed = tripStatisticsUpdater.getSmoothedSpeed() * UnitConversions.MS_TO_KMH;
|
||||
if (!metricUnits) {
|
||||
if (!chartView.getMetricUnits()) {
|
||||
speed *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
pace = speed == 0 ? 0.0 : 60.0 / speed;
|
||||
@@ -448,12 +435,12 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
|
||||
|
||||
@VisibleForTesting
|
||||
void setMetricUnits(boolean value) {
|
||||
metricUnits = value;
|
||||
chartView.setMetricUnits(value);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setReportSpeed(boolean value) {
|
||||
reportSpeed = value;
|
||||
chartView.setReportSpeed(value);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
public class ChartTimeFragment extends ChartFragment {
|
||||
|
||||
public ChartTimeFragment() {
|
||||
super(false);
|
||||
}
|
||||
}
|
||||
@@ -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 final 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;
|
||||
|
||||
@@ -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 final 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;
|
||||
|
||||
@@ -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 final int titleId;
|
||||
private static final String KEY_TITLE_ID = "titleId";
|
||||
private static final String KEY_OPTION_ID = "optionId";
|
||||
|
||||
private FileTypeCaller caller;
|
||||
private final 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,6 +68,9 @@ public class FileTypeDialogFragment extends DialogFragment {
|
||||
@Override
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
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++) {
|
||||
|
||||
@@ -57,7 +57,11 @@ 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_MS;
|
||||
|
||||
private MenuItem shareMarkerImageMenuItem;
|
||||
|
||||
private ContentProviderUtils contentProviderUtils;
|
||||
private Handler handler;
|
||||
private ImageView photoView;
|
||||
@@ -143,6 +147,7 @@ public class MarkerDetailFragment extends Fragment {
|
||||
// Need to update the waypoint in case returning after an edit
|
||||
updateWaypoint(true);
|
||||
updateUi();
|
||||
updateMenuItems();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -168,8 +173,14 @@ public class MarkerDetailFragment extends Fragment {
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.marker_detail, menu);
|
||||
|
||||
shareMarkerImageMenuItem = menu.findItem(R.id.marker_detail_share);
|
||||
updateWaypoint(false);
|
||||
updateMenuItems();
|
||||
}
|
||||
|
||||
private void updateMenuItems() {
|
||||
if (shareMarkerImageMenuItem != null)
|
||||
shareMarkerImageMenuItem.setVisible(waypoint.hasPhoto());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -186,6 +197,13 @@ public class MarkerDetailFragment extends Fragment {
|
||||
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.marker_detail_share:
|
||||
if (waypoint.hasPhoto()) {
|
||||
intent = IntentUtils.newShareImageIntent(getContext(), waypoint.getPhotoURI());
|
||||
intent = Intent.createChooser(intent, null);
|
||||
startActivity(intent);
|
||||
}
|
||||
return true;
|
||||
case R.id.marker_detail_delete:
|
||||
DeleteMarkerDialogFragment.showDialog(getChildFragmentManager(), new long[]{markerId});
|
||||
return true;
|
||||
|
||||
@@ -139,15 +139,10 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
private TextView speedMovingUnit;
|
||||
private View gradeElevationSeparator;
|
||||
private View gradeElevationContainer;
|
||||
private TextView elevationMinValue;
|
||||
private TextView elevationMinUnit;
|
||||
private TextView elevationMaxValue;
|
||||
private TextView elevationMaxUnit;
|
||||
private View speedContainer;
|
||||
private TextView speedLabel;
|
||||
private TextView speedValue;
|
||||
private TextView speedUnit;
|
||||
private View elevationContainer;
|
||||
private TextView elevationValue;
|
||||
private TextView elevationUnit;
|
||||
private View coordinateSeparator;
|
||||
@@ -188,18 +183,11 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
gradeElevationSeparator = view.findViewById(R.id.stats_elevation_separator);
|
||||
gradeElevationContainer = view.findViewById(R.id.stats_elevation_container);
|
||||
|
||||
elevationMinValue = view.findViewById(R.id.stats_elevation_min_value);
|
||||
elevationMinUnit = view.findViewById(R.id.stats_elevation_min_unit);
|
||||
|
||||
elevationMaxValue = view.findViewById(R.id.stats_elevation_max_value);
|
||||
elevationMaxUnit = view.findViewById(R.id.stats_elevation_max_unit);
|
||||
|
||||
speedContainer = view.findViewById(R.id.stats_speed);
|
||||
speedLabel = view.findViewById(R.id.stats_speed_label);
|
||||
speedValue = view.findViewById(R.id.stats_speed_value);
|
||||
speedUnit = view.findViewById(R.id.stats_speed_unit);
|
||||
|
||||
elevationContainer = view.findViewById(R.id.stats_elevation);
|
||||
elevationValue = view.findViewById(R.id.stats_elevation_current_value);
|
||||
elevationUnit = view.findViewById(R.id.stats_elevation_current_unit);
|
||||
|
||||
@@ -302,18 +290,11 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
gradeElevationSeparator = null;
|
||||
gradeElevationContainer = null;
|
||||
|
||||
elevationMinValue = null;
|
||||
elevationMinUnit = null;
|
||||
|
||||
elevationMaxValue = null;
|
||||
elevationMaxUnit = null;
|
||||
|
||||
speedContainer = null;
|
||||
speedLabel = null;
|
||||
speedValue = null;
|
||||
speedUnit = null;
|
||||
|
||||
elevationContainer = null;
|
||||
elevationValue = null;
|
||||
elevationUnit = null;
|
||||
|
||||
@@ -542,31 +523,10 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
speedMovingUnit.setText(parts.second);
|
||||
}
|
||||
|
||||
|
||||
// Set elevation
|
||||
// Make elevation visible?
|
||||
{
|
||||
boolean showElevation = PreferencesUtils.isShowStatsGradeElevation(getContext());
|
||||
gradeElevationSeparator.setVisibility(showElevation ? View.VISIBLE : View.GONE);
|
||||
|
||||
gradeElevationContainer.setVisibility(showElevation ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (showElevation) {
|
||||
{
|
||||
double elevation = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getMinElevation();
|
||||
Pair<String, String> parts = StringUtils.formatElevation(getContext(), elevation, metricUnits);
|
||||
|
||||
elevationMinValue.setText(parts.first);
|
||||
elevationMinUnit.setText(parts.second);
|
||||
}
|
||||
|
||||
{
|
||||
double elevation = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getMaxElevation();
|
||||
Pair<String, String> parts = StringUtils.formatElevation(getContext(), elevation, metricUnits);
|
||||
|
||||
elevationMaxValue.setText(parts.first);
|
||||
elevationMaxUnit.setText(parts.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,7 +558,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
|
||||
|
||||
// Set elevation
|
||||
boolean showGradeElevation = isRecording && PreferencesUtils.isShowStatsGradeElevation(getContext());
|
||||
elevationContainer.setVisibility(showGradeElevation ? View.VISIBLE : View.GONE);
|
||||
gradeElevationContainer.setVisibility(showGradeElevation ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (showGradeElevation) {
|
||||
double altitude = lastLocation != null && lastLocation.hasAltitude() ? lastLocation.getAltitude() : Double.NaN;
|
||||
|
||||
@@ -232,7 +232,9 @@ public enum TrackFileFormat {
|
||||
/**
|
||||
* Returns whether the format supports photos.
|
||||
*/
|
||||
public boolean includesPhotos() { return false; };
|
||||
public boolean includesPhotos() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of for each format.
|
||||
|
||||
@@ -32,7 +32,6 @@ 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);
|
||||
}
|
||||
|
||||
wakeLock = SystemUtils.acquireWakeLock(exportActivity, wakeLock);
|
||||
|
||||
return exportAllTracks();
|
||||
} finally {
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
}
|
||||
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ public class FileTrackExporter implements TrackExporter {
|
||||
// Write the current location
|
||||
trackWriter.writeLocation(location);
|
||||
if (trackExporterListener != null) {
|
||||
trackExporterListener.onProgressUpdate(locationNumber, track.getNumberOfPoints());
|
||||
trackExporterListener.onProgressUpdate(locationNumber, locationIterator.getCount());
|
||||
}
|
||||
} else {
|
||||
if (wroteSegment) {
|
||||
|
||||
@@ -39,11 +39,11 @@ import javax.xml.parsers.SAXParserFactory;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
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.TrackPointFactory;
|
||||
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.stats.TripStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
@@ -262,7 +262,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
trackData.tripStatisticsUpdater.updateTime(trackData.importTime);
|
||||
}
|
||||
trackData.track.setTripStatistics(trackData.tripStatisticsUpdater.getTripStatistics());
|
||||
trackData.track.setNumberOfPoints(trackData.numberOfLocations);
|
||||
contentProviderUtils.updateTrack(trackData.track);
|
||||
}
|
||||
|
||||
@@ -274,7 +273,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;
|
||||
}
|
||||
@@ -344,8 +343,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;
|
||||
|
||||
@@ -43,6 +43,7 @@ import de.dennisguse.opentracks.TrackDetailActivity;
|
||||
import de.dennisguse.opentracks.TrackListActivity;
|
||||
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;
|
||||
@@ -70,12 +71,6 @@ import de.dennisguse.opentracks.util.UnitConversions;
|
||||
*/
|
||||
public class TrackRecordingService extends Service {
|
||||
|
||||
// TODO Move to a different place.
|
||||
@Deprecated
|
||||
public static final double PAUSE_LATITUDE = 100.0;
|
||||
@Deprecated
|
||||
public static final double RESUME_LATITUDE = 200.0;
|
||||
|
||||
// Anything faster than that (in meters per second) will be considered moving.
|
||||
private static final String TAG = TrackRecordingService.class.getSimpleName();
|
||||
|
||||
@@ -386,7 +381,7 @@ 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);
|
||||
}
|
||||
@@ -444,7 +439,7 @@ public class TrackRecordingService extends Service {
|
||||
insertLocation(track, lastLocation, getLastValidTrackPointInCurrentSegment(trackId));
|
||||
|
||||
// Update the recording track time
|
||||
updateRecordingTrack(track, false);
|
||||
updateRecordingTrack(track);
|
||||
}
|
||||
|
||||
String trackName = TrackNameUtils.getTrackName(this, trackId, track.getTripStatistics().getStartTime());
|
||||
@@ -473,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);
|
||||
}
|
||||
@@ -610,7 +605,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(lastLocation.getTime());
|
||||
insertLocation(track, pause, null);
|
||||
|
||||
@@ -655,7 +650,7 @@ public class TrackRecordingService extends Service {
|
||||
try {
|
||||
contentProviderUtils.insertTrackPoint(location, track.getId());
|
||||
trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
|
||||
updateRecordingTrack(track, LocationUtils.isValidLocation(location));
|
||||
updateRecordingTrack(track);
|
||||
} catch (SQLiteException e) {
|
||||
/*
|
||||
* Insert failed, most likely because of SqlLite error code 5 (SQLite_BUSY).
|
||||
@@ -668,16 +663,10 @@ public class TrackRecordingService extends Service {
|
||||
|
||||
/**
|
||||
* Updates the recording track time.
|
||||
* Increase the number of points if it is a new and valid track point.
|
||||
*
|
||||
* @param track the track
|
||||
* @param increaseNumberOfPoints true to increase the number of points
|
||||
* @param track the track
|
||||
*/
|
||||
private void updateRecordingTrack(Track track, boolean increaseNumberOfPoints) {
|
||||
if (increaseNumberOfPoints) {
|
||||
track.setNumberOfPoints(track.getNumberOfPoints() + 1);
|
||||
}
|
||||
|
||||
private void updateRecordingTrack(Track track) {
|
||||
trackTripStatisticsUpdater.updateTime(System.currentTimeMillis());
|
||||
track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics());
|
||||
contentProviderUtils.updateTrack(track);
|
||||
@@ -713,17 +702,7 @@ public class TrackRecordingService extends Service {
|
||||
}
|
||||
|
||||
private void showNotification(boolean isGpsStarted) {
|
||||
// Dijkstra If
|
||||
if (isRecording() && isPaused()) {
|
||||
stopForeground(true);
|
||||
notificationManager.cancelNotification();
|
||||
|
||||
}
|
||||
if (!isRecording() && !isGpsStarted) {
|
||||
stopForeground(true);
|
||||
notificationManager.cancelNotification();
|
||||
}
|
||||
if (isRecording() && !isPaused()) {
|
||||
if (isRecording()) {
|
||||
Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
|
||||
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId);
|
||||
PendingIntent pendingIntent = TaskStackBuilder.create(this)
|
||||
@@ -745,5 +724,9 @@ public class TrackRecordingService extends Service {
|
||||
notificationManager.updateContent(getString(R.string.gps_starting));
|
||||
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.getNotification());
|
||||
}
|
||||
if (!isRecording() && !isGpsStarted) {
|
||||
stopForeground(true);
|
||||
notificationManager.cancelNotification();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,9 @@ import android.util.Log;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.stats.TripStatistics;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
import de.dennisguse.opentracks.util.AnnouncementUtils;
|
||||
|
||||
/**
|
||||
* This class will periodically announce the user's trip statistics.
|
||||
@@ -160,7 +157,8 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
|
||||
Log.i(TAG, "Speech is not allowed at this time.");
|
||||
return;
|
||||
}
|
||||
speakAnnouncement(getAnnouncement(tripStatistics));
|
||||
String announcement = AnnouncementUtils.getAnnouncement(context, tripStatistics);
|
||||
speakAnnouncement(announcement);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -169,8 +167,6 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
|
||||
tts.shutdown();
|
||||
tts = null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void onTtsReady() {
|
||||
@@ -196,81 +192,4 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
|
||||
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
|
||||
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
|
||||
}
|
||||
|
||||
private String getAnnouncement(TripStatistics tripStatistics) {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(context);
|
||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(context);
|
||||
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
|
||||
double distancePerTime = tripStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
|
||||
|
||||
if (distance == 0) {
|
||||
return context.getString(R.string.voice_total_distance_zero);
|
||||
}
|
||||
|
||||
if (!metricUnits) {
|
||||
distance *= UnitConversions.KM_TO_MI;
|
||||
distancePerTime *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
String rate;
|
||||
if (reportSpeed) {
|
||||
int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour;
|
||||
rate = context.getResources().getQuantityString(speedId, getQuantityCount(distancePerTime), distancePerTime);
|
||||
} else {
|
||||
double timePerDistance = distancePerTime == 0 ? 0.0 : 1 / distancePerTime;
|
||||
int paceId = metricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
|
||||
long time = Math.round(timePerDistance * UnitConversions.HR_TO_MIN * UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
|
||||
rate = context.getString(paceId, getAnnounceTime(time));
|
||||
}
|
||||
|
||||
int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers : R.plurals.voiceTotalDistanceMiles;
|
||||
String totalDistance = context.getResources().getQuantityString(totalDistanceId, getQuantityCount(distance), distance);
|
||||
|
||||
return context.getString(R.string.voice_template, totalDistance,
|
||||
getAnnounceTime(tripStatistics.getMovingTime()), rate);
|
||||
}
|
||||
|
||||
private String getAnnounceTime(long time) {
|
||||
int[] parts = StringUtils.getTimeParts(time);
|
||||
String seconds = context.getResources()
|
||||
.getQuantityString(R.plurals.voiceSeconds, parts[0], parts[0]);
|
||||
String minutes = context.getResources()
|
||||
.getQuantityString(R.plurals.voiceMinutes, parts[1], parts[1]);
|
||||
String hours = context.getResources()
|
||||
.getQuantityString(R.plurals.voiceHours, parts[2], parts[2]);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (parts[2] != 0) {
|
||||
sb.append(hours);
|
||||
sb.append(" ");
|
||||
}
|
||||
sb.append(minutes);
|
||||
sb.append(" ");
|
||||
sb.append(seconds);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plural count to be used by getQuantityString.
|
||||
* getQuantityString only supports integer quantities, not a double quantity like "2.2".
|
||||
* <p>
|
||||
* As a temporary workaround, we convert a double quantity to an integer quantity.
|
||||
* If the double quantity is exactly 0, 1, or 2, then we can return these integer quantities.
|
||||
* Otherwise, we cast the double quantity to an integer quantity.
|
||||
* However, we need to make sure that if the casted value is 0, 1, or 2, we don't return those, instead, return the next biggest integer 3.
|
||||
*
|
||||
* @param d the double value
|
||||
*/
|
||||
private int getQuantityCount(double d) {
|
||||
if (d == 0) {
|
||||
return 0;
|
||||
} else if (d == 1) {
|
||||
return 1;
|
||||
} else if (d == 2) {
|
||||
return 2;
|
||||
} else {
|
||||
//TODO This seems weird; why not use Math.round(d) or Math.ceil()?
|
||||
int count = (int) d;
|
||||
return count < 3 ? 3 : count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public class TripStatistics {
|
||||
// The min and max elevation (meters) seen on this trip.
|
||||
private final ExtremityMonitor elevationExtremities = new ExtremityMonitor();
|
||||
// The min and max grade seen on this trip.
|
||||
@Deprecated //Remove grade min/max completely
|
||||
private final ExtremityMonitor gradeExtremities = new ExtremityMonitor();
|
||||
|
||||
// The trip start time. This is the system time, might not match the GPs time.
|
||||
|
||||
@@ -21,11 +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}.
|
||||
@@ -123,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));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.stats.TripStatistics;
|
||||
|
||||
public class AnnouncementUtils {
|
||||
|
||||
private AnnouncementUtils() {}
|
||||
|
||||
public static String getAnnouncement(Context context, TripStatistics tripStatistics) {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(context);
|
||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(context);
|
||||
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
|
||||
double distancePerTime = tripStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
|
||||
|
||||
if (distance == 0) {
|
||||
return context.getString(R.string.voice_total_distance_zero);
|
||||
}
|
||||
|
||||
if (!metricUnits) {
|
||||
distance *= UnitConversions.KM_TO_MI;
|
||||
distancePerTime *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
String rate;
|
||||
if (reportSpeed) {
|
||||
int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour;
|
||||
rate = context.getResources().getQuantityString(speedId, getQuantityCount(distancePerTime), distancePerTime);
|
||||
} else {
|
||||
double timePerDistance = distancePerTime == 0 ? 0.0 : 1 / distancePerTime;
|
||||
int paceId = metricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
|
||||
long time = Math.round(timePerDistance * UnitConversions.HR_TO_MIN * UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
|
||||
rate = context.getString(paceId, getAnnounceTime(context, time));
|
||||
}
|
||||
|
||||
int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers : R.plurals.voiceTotalDistanceMiles;
|
||||
String totalDistance = context.getResources().getQuantityString(totalDistanceId, getQuantityCount(distance), distance);
|
||||
|
||||
return context.getString(R.string.voice_template, totalDistance, getAnnounceTime(context, tripStatistics.getMovingTime()), rate);
|
||||
}
|
||||
|
||||
public static String getAnnounceTime(Context context, long time) {
|
||||
int[] parts = StringUtils.getTimeParts(time);
|
||||
String seconds = context.getResources()
|
||||
.getQuantityString(R.plurals.voiceSeconds, parts[0], parts[0]);
|
||||
String minutes = context.getResources()
|
||||
.getQuantityString(R.plurals.voiceMinutes, parts[1], parts[1]);
|
||||
String hours = context.getResources()
|
||||
.getQuantityString(R.plurals.voiceHours, parts[2], parts[2]);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (parts[2] != 0) {
|
||||
sb.append(hours);
|
||||
sb.append(" ");
|
||||
}
|
||||
sb.append(minutes);
|
||||
sb.append(" ");
|
||||
sb.append(seconds);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plural count to be used by getQuantityString.
|
||||
* getQuantityString only supports integer quantities, not a double quantity like "2.2".
|
||||
* <p>
|
||||
* As a temporary workaround, we convert a double quantity to an integer quantity.
|
||||
* If the double quantity is exactly 0, 1, or 2, then we can return these integer quantities.
|
||||
* Otherwise, we cast the double quantity to an integer quantity.
|
||||
* However, we need to make sure that if the casted value is 0, 1, or 2, we don't return those, instead, return the next biggest integer 3.
|
||||
*
|
||||
* @param d the double value
|
||||
*/
|
||||
public static int getQuantityCount(double d) {
|
||||
if (d == 0) {
|
||||
return 0;
|
||||
} else if (d == 1) {
|
||||
return 1;
|
||||
} else if (d == 2) {
|
||||
return 2;
|
||||
} else {
|
||||
//TODO This seems weird; why not use Math.round(d) or Math.ceil()?
|
||||
int count = (int) d;
|
||||
return count < 3 ? 3 : count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,21 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
import de.dennisguse.opentracks.BuildConfig;
|
||||
|
||||
@@ -31,6 +41,8 @@ import de.dennisguse.opentracks.BuildConfig;
|
||||
*/
|
||||
public class FileUtils {
|
||||
|
||||
private static final String TAG = FileUtils.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Used to transfer picture from the camera.
|
||||
*/
|
||||
@@ -38,6 +50,8 @@ public class FileUtils {
|
||||
|
||||
public static final String EXPORT_DIR = "OpenTracks";
|
||||
|
||||
private static final String JPEG_EXTENSION = "jpeg";
|
||||
|
||||
/**
|
||||
* The maximum FAT32 path length. See the FAT32 spec at
|
||||
* http://msdn.microsoft.com/en-us/windows/hardware/gg463080
|
||||
@@ -196,4 +210,46 @@ public class FileUtils {
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the image's absolute path from a track identified by trackId.
|
||||
*
|
||||
* @param context the context.
|
||||
* @param trackId the track id.
|
||||
*/
|
||||
public static String getImageUrl(Context context, long trackId) {
|
||||
File dir = FileUtils.getPhotoDir(context, trackId);
|
||||
|
||||
String fileName = SimpleDateFormat.getDateTimeInstance().format(new Date());
|
||||
File file = new File(dir, FileUtils.buildUniqueFileName(dir, fileName, JPEG_EXTENSION));
|
||||
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a File (src) to a File (dst).
|
||||
*
|
||||
* @param src source file.
|
||||
* @param dst destination file.
|
||||
* @throws IOException
|
||||
*/
|
||||
public static void copy(FileDescriptor src, File dst) throws IOException {
|
||||
try (FileChannel in = new FileInputStream(src).getChannel();
|
||||
FileChannel out = new FileOutputStream(dst).getChannel()) {
|
||||
in.transferTo(0, in.size(), out);
|
||||
} catch (Exception e) {
|
||||
// post to log
|
||||
Log.e(TAG, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Uri for the file.
|
||||
*
|
||||
* @param context the context.
|
||||
* @param file the file.
|
||||
*/
|
||||
public static Uri getUriForFile(Context context, File file) {
|
||||
return FileProvider.getUriForFile(context, FileUtils.FILEPROVIDER, file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 startDashboard(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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -75,9 +75,12 @@ public class IntentUtils {
|
||||
if (trackIds.length == 0) {
|
||||
throw new RuntimeException("Need to share at least one track.");
|
||||
}
|
||||
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
|
||||
String trackDescription = "";
|
||||
if (trackIds.length == 1) {
|
||||
Track track = new ContentProviderUtils(context).getTrack(trackIds[0]);
|
||||
Track track = contentProviderUtils.getTrack(trackIds[0]);
|
||||
trackDescription = track == null ? "" : new DescriptionGenerator(context).generateTrackDescription(track, false);
|
||||
}
|
||||
|
||||
@@ -86,7 +89,12 @@ public class IntentUtils {
|
||||
|
||||
ArrayList<Uri> uris = new ArrayList<>();
|
||||
for (long trackId : trackIds) {
|
||||
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(new long[]{trackId}, TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES);
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
if (track == null) {
|
||||
Log.e(TAG, "TrackId " + trackId + " could not be resolved.");
|
||||
}
|
||||
|
||||
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(new long[]{trackId}, track.getName(), TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES);
|
||||
uris.add(uriAndMime.first);
|
||||
mime = uriAndMime.second;
|
||||
}
|
||||
@@ -99,6 +107,30 @@ public class IntentUtils {
|
||||
.putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_track_share_file_body, trackDescription));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an intent to share a waypoint image with an app.
|
||||
*
|
||||
* @param context the context.
|
||||
* @param uri uri with the image to share.
|
||||
*/
|
||||
public static Intent newShareImageIntent(Context context, Uri uri) {
|
||||
String mime = context.getContentResolver().getType(uri);
|
||||
|
||||
/*
|
||||
* Because the #166 bug, when you import KMZ tracks then it creates file:/// from markers with photo.
|
||||
* The photos should be content:/// not file:/// because getType(uri) always returns null for file:///
|
||||
* In .setType, to avoid side effects because the #166 bug described above it checks if mime is null.
|
||||
* If it is then it hardcode "images/*".
|
||||
*/
|
||||
return new Intent(Intent.ACTION_SEND)
|
||||
.setType(mime != null ? mime : "image/*")
|
||||
.setAction(Intent.ACTION_SEND)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.share_image_subject))
|
||||
.putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_image_body));
|
||||
}
|
||||
|
||||
public static void showCoordinateOnMap(Context context, Waypoint waypoint) {
|
||||
showCoordinateOnMap(context, waypoint.getLocation().getLatitude(), waypoint.getLocation().getLongitude(), waypoint.getName());
|
||||
}
|
||||
@@ -128,7 +160,6 @@ public class IntentUtils {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Send intent to show tracks on a map (needs an another app).
|
||||
*
|
||||
@@ -140,14 +171,14 @@ public class IntentUtils {
|
||||
return;
|
||||
}
|
||||
|
||||
if (IntentDashboardUtils.startDashboard(context, trackIds[0])) {
|
||||
if (IntentDashboardUtils.startDashboard(context, trackIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra(TrackPointsColumns.TRACKID, trackIds[0]);
|
||||
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackIds, TrackFileFormat.KMZ_WITH_TRACKDETAIL);
|
||||
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackIds, "SharingTrack", TrackFileFormat.KMZ_WITH_TRACKDETAIL);
|
||||
intent.setDataAndType(uriAndMime.first, uriAndMime.second);
|
||||
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@android:color/white"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M3,4V1h2v3h3v2H5v3H3V6H0V4H3zM6,10V7h3V4h7l1.83,2H21c1.1,0 2,0.9 2,2v12c0,1.1 -0.9,2 -2,2H5c-1.1,0 -2,-0.9 -2,-2V10H6zM13,19c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5s-5,2.24 -5,5S10.24,19 13,19zM9.8,14c0,1.77 1.43,3.2 3.2,3.2s3.2,-1.43 3.2,-3.2s-1.43,-3.2 -3.2,-3.2S9.8,12.23 9.8,14z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@android:color/white"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19,7v2.99s-1.99,0.01 -2,0L17,7h-3s0.01,-1.99 0,-2h3L17,2h2v3h3v2h-3zM16,11L16,8h-3L13,5L5,5c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-8h-3zM5,19l3,-4 2,3 3,-4 4,5L5,19z"/>
|
||||
</vector>
|
||||
@@ -1,5 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z"/>
|
||||
</vector>
|
||||
@@ -1,5 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M15,1L9,1v2h6L15,1zM11,14h2L13,8h-2v6zM19.03,7.39l1.42,-1.42c-0.43,-0.51 -0.9,-0.99 -1.41,-1.41l-1.42,1.42C16.07,4.74 14.12,4 12,4c-4.97,0 -9,4.03 -9,9s4.02,9 9,9 9,-4.03 9,-9c0,-2.12 -0.74,-4.07 -1.97,-5.61zM12,20c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
|
||||
</vector>
|
||||
@@ -1,5 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM9,17L7,17v-7h2v7zM13,17h-2L11,7h2v10zM17,17h-2v-4h2v4z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid
|
||||
android:color="@color/text_gradient_center"/>
|
||||
</shape>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -67,6 +67,39 @@ limitations under the License.
|
||||
android:imeOptions="actionDone"
|
||||
android:importantForAutofill="no" />
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/marker_edit_waypoint_photo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:scaleType="fitStart"
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="@string/image_photo_marker" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/marker_edit_waypoint_photo_delete"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:background="@drawable/shape_circle_darkgray"
|
||||
android:padding="4dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:src="@drawable/ic_delete_forever_24dp" />
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
@@ -209,98 +209,19 @@ limitations under the License.
|
||||
android:layout_gravity="center">
|
||||
|
||||
<!-- Current elevation -->
|
||||
<TableRow
|
||||
android:id="@+id/stats_elevation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<LinearLayout
|
||||
style="@style/StatsSmallValueContainer"
|
||||
android:layout_gravity="start|bottom">
|
||||
|
||||
<LinearLayout
|
||||
style="@style/StatsSmallValueContainer"
|
||||
android:layout_gravity="start|bottom">
|
||||
<TextView
|
||||
android:id="@+id/stats_elevation_current_value"
|
||||
style="@style/StatsSmallValue"
|
||||
android:value="@string/value_unknown" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_elevation_current_value"
|
||||
style="@style/StatsSmallValue"
|
||||
android:value="@string/value_unknown" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_elevation_current_unit"
|
||||
style="@style/StatsUnit" />
|
||||
</LinearLayout>
|
||||
</TableRow>
|
||||
|
||||
<!-- Minimal elevation -->
|
||||
<TableRow
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
style="@style/StatsSmallValueContainer"
|
||||
android:layout_gravity="start|bottom">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_elevation_min_value"
|
||||
style="@style/StatsSmallValue"
|
||||
android:value="@string/value_unknown" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_elevation_min_unit"
|
||||
style="@style/StatsUnit" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
style="@style/StatsSmallValueContainer"
|
||||
android:layout_gravity="start|bottom">
|
||||
|
||||
<TextView
|
||||
style="@style/StatsSmallValue"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<TextView
|
||||
style="@style/StatsSmallLabel"
|
||||
android:layout_gravity="bottom"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/stats_min" />
|
||||
</LinearLayout>
|
||||
</TableRow>
|
||||
|
||||
<!-- Maximal elevation -->
|
||||
<TableRow
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
style="@style/StatsSmallValueContainer"
|
||||
android:layout_gravity="start|bottom">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_elevation_max_value"
|
||||
style="@style/StatsSmallValue" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_elevation_max_unit"
|
||||
style="@style/StatsUnit" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
style="@style/StatsSmallValueContainer"
|
||||
android:layout_gravity="start|bottom">
|
||||
|
||||
<TextView
|
||||
style="@style/StatsSmallValue"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<TextView
|
||||
style="@style/StatsSmallLabel"
|
||||
android:layout_gravity="bottom"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/stats_max" />
|
||||
|
||||
</LinearLayout>
|
||||
</TableRow>
|
||||
<TextView
|
||||
android:id="@+id/stats_elevation_current_unit"
|
||||
style="@style/StatsUnit" />
|
||||
</LinearLayout>
|
||||
</TableLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -20,6 +20,11 @@ limitations under the License.
|
||||
android:icon="@drawable/ic_map_24dp"
|
||||
android:title="@string/menu_show_on_map"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/marker_detail_share"
|
||||
android:icon="@drawable/ic_share_24dp"
|
||||
android:title="@string/menu_share"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/marker_detail_edit"
|
||||
android:icon="@drawable/ic_edit_24dp"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/marker_edit_insert_photo"
|
||||
android:icon="@drawable/ic_add_a_photo_24dp"
|
||||
android:orderInCategory="2"
|
||||
app:showAsAction="ifRoom"
|
||||
android:title="@string/menu_insert_photo" />
|
||||
<item
|
||||
android:id="@+id/marker_edit_insert_gallery_img"
|
||||
android:icon="@drawable/ic_add_photo_alternate_24dp"
|
||||
android:orderInCategory="2"
|
||||
app:showAsAction="ifRoom"
|
||||
android:title="@string/menu_insert_gallery_img" />
|
||||
</menu>
|
||||
@@ -21,12 +21,6 @@ limitations under the License.
|
||||
android:orderInCategory="1"
|
||||
app:showAsAction="ifRoom"
|
||||
android:title="@string/menu_insert_marker" />
|
||||
<item
|
||||
android:id="@+id/track_detail_insert_photo"
|
||||
android:icon="@drawable/ic_camera_24dp"
|
||||
android:orderInCategory="2"
|
||||
app:showAsAction="ifRoom"
|
||||
android:title="@string/menu_insert_photo" />
|
||||
<item
|
||||
android:id="@+id/track_detail_menu_show_on_map"
|
||||
android:icon="@drawable/ic_map_24dp"
|
||||
|
||||
@@ -262,7 +262,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">مستشعر بلوتوث</string>
|
||||
<string name="settings_sensor_connected">تم التوصيل بـ %1$s</string>
|
||||
<string name="settings_stats_coordinate">خط العرض/خط الطول</string>
|
||||
<string name="settings_stats_grade_elevation">الدرجة/الارتفاع</string>
|
||||
<string name="settings_stats_rate_title">المعدل المفضل</string>
|
||||
<string name="settings_stats_units_imperial">وحدات القياس البريطانية (ميل، قدم)</string>
|
||||
<string name="settings_stats_units_metric">وحدات القياس المترية (كم، م)</string>
|
||||
|
||||
@@ -262,7 +262,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Bluetooth sensor</string>
|
||||
<string name="settings_sensor_connected">Connected to %1$s</string>
|
||||
<string name="settings_stats_coordinate">Latitude/Longitude</string>
|
||||
<string name="settings_stats_grade_elevation">Grade/elevation</string>
|
||||
<string name="settings_stats_rate_title">Preferred rate</string>
|
||||
<string name="settings_stats_units_imperial">Imperial (miles, ft)</string>
|
||||
<string name="settings_stats_units_metric">Metric (km, m)</string>
|
||||
|
||||
@@ -192,6 +192,7 @@
|
||||
<string name="menu_import_all">Importar todo</string>
|
||||
<string name="menu_insert_marker">Insertar marcador</string>
|
||||
<string name="menu_insert_photo">Insertar foto</string>
|
||||
<string name="menu_insert_gallery_img">Insertar imagen de galería</string>
|
||||
<string name="menu_map">Mapa</string>
|
||||
<string name="menu_markers">Marcadores</string>
|
||||
<string name="menu_play">Reproducir</string>
|
||||
@@ -262,7 +263,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Sensor de Bluetooth</string>
|
||||
<string name="settings_sensor_connected">Conectado a %1$s</string>
|
||||
<string name="settings_stats_coordinate">Latitud/longitud</string>
|
||||
<string name="settings_stats_grade_elevation">Pendiente/elevación</string>
|
||||
<string name="settings_stats_rate_title">Tasa preferida</string>
|
||||
<string name="settings_stats_units_imperial">Imperiales (millas, pies)</string>
|
||||
<string name="settings_stats_units_metric">Métricas (km, m)</string>
|
||||
@@ -446,4 +446,7 @@
|
||||
<string name="waypoint_type_waterfall">cascada</string>
|
||||
<string name="waypoint_type_water_fountain">fuente de agua</string>
|
||||
<string name="waypoint_type_waypoint">punto de referencia</string>
|
||||
<!-- Share image -->
|
||||
<string name="share_image_subject">Me gustaría compartir una imagen contigo</string>
|
||||
<string name="share_image_body">Creo que esta imagen podría interesarte.</string>
|
||||
</resources>
|
||||
|
||||
@@ -263,7 +263,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Bluetooth сензор</string>
|
||||
<string name="settings_sensor_connected">Свързан с %1$s</string>
|
||||
<string name="settings_stats_coordinate">Ширина и дължина</string>
|
||||
<string name="settings_stats_grade_elevation">Наклон/кота</string>
|
||||
<string name="settings_stats_rate_title">Предпочитана мярка</string>
|
||||
<string name="settings_stats_units_imperial">Имперски (мил, фт)</string>
|
||||
<string name="settings_stats_units_metric">Метрични (км, м)</string>
|
||||
|
||||
@@ -265,7 +265,6 @@ es vegi el cel. Torneu-ho a provar més tard.</string>
|
||||
<string name="settings_sensor_bluetooth_sensor">Sensor de Bluetooth</string>
|
||||
<string name="settings_sensor_connected">Connectat a %1$s</string>
|
||||
<string name="settings_stats_coordinate">Latitud/Longitud:</string>
|
||||
<string name="settings_stats_grade_elevation">Desnivell/elevació</string>
|
||||
<string name="settings_stats_rate_title">Mesura preferida</string>
|
||||
<string name="settings_stats_units_imperial">Sistema anglosaxó (milles, peus)</string>
|
||||
<string name="settings_stats_units_metric">Sistema mètric (km, m)</string>
|
||||
@@ -449,4 +448,7 @@ es vegi el cel. Torneu-ho a provar més tard.</string>
|
||||
<string name="waypoint_type_waterfall">salt d\'aigua</string>
|
||||
<string name="waypoint_type_water_fountain">font d\'aigua</string>
|
||||
<string name="waypoint_type_waypoint">punt de ruta</string>
|
||||
<!-- Share image -->
|
||||
<string name="share_image_subject">Vull compartir una imatge amb tu</string>
|
||||
<string name="share_image_body">Crec que aquesta imatge et podria interessar.</string>
|
||||
</resources>
|
||||
|
||||
@@ -262,7 +262,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Senzor Bluetooth</string>
|
||||
<string name="settings_sensor_connected">Připojeno k senzoru %1$s</string>
|
||||
<string name="settings_stats_coordinate">Zeměpisná šířka/délka</string>
|
||||
<string name="settings_stats_grade_elevation">Sklon/Převýšení</string>
|
||||
<string name="settings_stats_rate_title">Preferovaná rychlost</string>
|
||||
<string name="settings_stats_units_imperial">Imperiální jednotky (míle, stopy)</string>
|
||||
<string name="settings_stats_units_metric">Metrické jednotky (km, m)</string>
|
||||
|
||||
@@ -265,7 +265,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Bluetooth-sensor</string>
|
||||
<string name="settings_sensor_connected">Tilsluttet %1$s</string>
|
||||
<string name="settings_stats_coordinate">Breddegrad/længdegrad</string>
|
||||
<string name="settings_stats_grade_elevation">Hældning/stigning</string>
|
||||
<string name="settings_stats_rate_title">Foretrukkenfartenhed</string>
|
||||
<string name="settings_stats_units_imperial">Britisk (mil, ft)</string>
|
||||
<string name="settings_stats_units_metric">Metrisk (km, m)</string>
|
||||
|
||||
@@ -265,7 +265,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Bluetooth-Sensor</string>
|
||||
<string name="settings_sensor_connected">Verbunden mit %1$s</string>
|
||||
<string name="settings_stats_coordinate">Breiten-/Längengrad</string>
|
||||
<string name="settings_stats_grade_elevation">Gefälle/Höhe</string>
|
||||
<string name="settings_stats_rate_title">Bevorzugtes Maß</string>
|
||||
<string name="settings_stats_units_imperial">Englisch (mi, ft)</string>
|
||||
<string name="settings_stats_units_metric">Metrisch (km, m)</string>
|
||||
|
||||
@@ -263,7 +263,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Αισθητήρας Bluetooth</string>
|
||||
<string name="settings_sensor_connected">Έγινε σύνδεση με %1$s</string>
|
||||
<string name="settings_stats_coordinate">Πλάτος/Μήκος</string>
|
||||
<string name="settings_stats_grade_elevation">Κλίση/Ανύψωση</string>
|
||||
<string name="settings_stats_rate_title">Προτιμώμενος ρυθμός</string>
|
||||
<string name="settings_stats_units_imperial">Αγγλοσαξονικό (μίλια, πόδια)</string>
|
||||
<string name="settings_stats_units_metric">Μετρικό (χλμ, μέτρα)</string>
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
<string name="marker_add_error">No se puede insertar el marcador. No se recibe señal de GPS. Inténtalo de nuevo.</string>
|
||||
<string name="marker_add_success">Se ha insertado un marcador</string>
|
||||
<string name="marker_add_canceled">Cancelado, no se ha añadido ningún marcador</string>
|
||||
<string name="marker_add_photo_canceled">Cancelado, no se ha añadido la foto al marcador</string>
|
||||
<string name="marker_delete_all_confirm_message">Todos los marcadores de esta ruta se eliminarán del dispositivo de forma permanente</string>
|
||||
<string name="marker_delete_one_confirm_message">El marcador se eliminará del dispositivo de forma permanente</string>
|
||||
<string name="marker_delete_one_confirm_title">¿Eliminar marcador?</string>
|
||||
@@ -195,6 +196,7 @@
|
||||
<string name="menu_import_all">Importar todo</string>
|
||||
<string name="menu_insert_marker">Insertar marcador</string>
|
||||
<string name="menu_insert_photo">Insertar foto</string>
|
||||
<string name="menu_insert_gallery_img">Insertar imagen de galería</string>
|
||||
<string name="menu_map">Mapa</string>
|
||||
<string name="menu_markers">Marcadores</string>
|
||||
<string name="menu_play">Reproducir</string>
|
||||
@@ -265,7 +267,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Sensor Bluetooth</string>
|
||||
<string name="settings_sensor_connected">Conectado a %1$s</string>
|
||||
<string name="settings_stats_coordinate">Latitud/Longitud</string>
|
||||
<string name="settings_stats_grade_elevation">Pendiente/Elevación</string>
|
||||
<string name="settings_stats_rate_title">Ritmo preferido</string>
|
||||
<string name="settings_stats_units_imperial">Imperiales (millas, pies)</string>
|
||||
<string name="settings_stats_units_metric">Métricas (km, m)</string>
|
||||
@@ -449,4 +450,7 @@
|
||||
<string name="waypoint_type_waterfall">cascada</string>
|
||||
<string name="waypoint_type_water_fountain">fuente de agua</string>
|
||||
<string name="waypoint_type_waypoint">Punto de ruta</string>
|
||||
<!-- Share image -->
|
||||
<string name="share_image_subject">Me gustaría compartir una imagen contigo</string>
|
||||
<string name="share_image_body">Creo que esta imagen te podría interesar.</string>
|
||||
</resources>
|
||||
|
||||
@@ -262,7 +262,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Bluetoothi andur</string>
|
||||
<string name="settings_sensor_connected">Ühendatud anduriga %1$s</string>
|
||||
<string name="settings_stats_coordinate">Laiuskraad/pikkuskraad</string>
|
||||
<string name="settings_stats_grade_elevation">Kalle/tõus</string>
|
||||
<string name="settings_stats_rate_title">Eelistatud määr</string>
|
||||
<string name="settings_stats_units_imperial">Inglise mõõtühikud (miili, jalga)</string>
|
||||
<string name="settings_stats_units_metric">Meetermõõdustik (km, m)</string>
|
||||
|
||||
@@ -262,7 +262,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Bluetooth sentsorea</string>
|
||||
<string name="settings_sensor_connected">Honekin konektatuta: %1$s</string>
|
||||
<string name="settings_stats_coordinate">Latitudea/Longitudea</string>
|
||||
<string name="settings_stats_grade_elevation">Inklinazioa/Altitudea</string>
|
||||
<string name="settings_stats_rate_title">Erritmo hobetsia</string>
|
||||
<string name="settings_stats_units_imperial">Inperiala (mi, ft)</string>
|
||||
<string name="settings_stats_units_metric">Metrikoa (km, m)</string>
|
||||
|
||||
@@ -262,7 +262,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">حسگر بلوتوث</string>
|
||||
<string name="settings_sensor_connected">به %1$s متصل شد</string>
|
||||
<string name="settings_stats_coordinate">عرض جغرافیایی/ طول جغرافیایی</string>
|
||||
<string name="settings_stats_grade_elevation">درجه / ارتفاع</string>
|
||||
<string name="settings_stats_rate_title">اولویت سنجه سرعت</string>
|
||||
<string name="settings_stats_units_imperial">انگلیسی (مایل، فوت)</string>
|
||||
<string name="settings_stats_units_metric">متریک (کیلومتر، متر)</string>
|
||||
|
||||
@@ -265,7 +265,6 @@ jotta ne voidaan tuoda kohteeseen %1$s.</string>
|
||||
<string name="settings_sensor_bluetooth_sensor">Bluetooth-anturi</string>
|
||||
<string name="settings_sensor_connected">Yhdistetty: %1$s</string>
|
||||
<string name="settings_stats_coordinate">Leveys-/pituusaste</string>
|
||||
<string name="settings_stats_grade_elevation">Korkeustaso/korkeus</string>
|
||||
<string name="settings_stats_rate_title">Haluttu nopeus</string>
|
||||
<string name="settings_stats_units_imperial">Brittiläinen järj.
|
||||
(maili, jalka)</string>
|
||||
|
||||
@@ -262,7 +262,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Capteur Bluetooth</string>
|
||||
<string name="settings_sensor_connected">Connecté à %1$s</string>
|
||||
<string name="settings_stats_coordinate">Latitude/longitude</string>
|
||||
<string name="settings_stats_grade_elevation">Inclinaison/Altitude</string>
|
||||
<string name="settings_stats_rate_title">Mode de calcul souhaité</string>
|
||||
<string name="settings_stats_units_imperial">Unités impériales (mi, pi)</string>
|
||||
<string name="settings_stats_units_metric">Unités métriques (km, m)</string>
|
||||
|
||||
@@ -264,7 +264,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Capteur Bluetooth</string>
|
||||
<string name="settings_sensor_connected">Connecté à %1$s</string>
|
||||
<string name="settings_stats_coordinate">Latitude/Longitude</string>
|
||||
<string name="settings_stats_grade_elevation">Inclinaison/Altitude</string>
|
||||
<string name="settings_stats_rate_title">Mode de calcul souhaité</string>
|
||||
<string name="settings_stats_units_imperial">Unités impériales (mi, pi)</string>
|
||||
<string name="settings_stats_units_metric">Unités métriques (km, m)</string>
|
||||
|
||||
@@ -264,7 +264,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Capteur Bluetooth</string>
|
||||
<string name="settings_sensor_connected">Connecté à %1$s</string>
|
||||
<string name="settings_stats_coordinate">Latitude/Longitude</string>
|
||||
<string name="settings_stats_grade_elevation">Inclinaison/Altitude</string>
|
||||
<string name="settings_stats_rate_title">Mode de calcul souhaité</string>
|
||||
<string name="settings_stats_units_imperial">Unités impériales (mi, pi)</string>
|
||||
<string name="settings_stats_units_metric">Unités métriques (km, m)</string>
|
||||
@@ -448,4 +447,11 @@
|
||||
<string name="waypoint_type_waterfall">cascade</string>
|
||||
<string name="waypoint_type_water_fountain">fontaine</string>
|
||||
<string name="waypoint_type_waypoint">point de cheminement</string>
|
||||
<string name="settings_recording_show_on_lockscreen_while_recording_title">Afficher les statistiques sur l\'écran de verrouillage</string>
|
||||
<string name="settings_recording_show_on_lockscreen_while_recording_summary">Pendant l\'enregistrement, afficher les statistiques sans déverrouiller l\'appareil.</string>
|
||||
<string name="settings_recording_keep_screen_on_while_recording_title">Garder l\'écran allumé</string>
|
||||
<string name="settings_recording_keep_screen_on_while_recording_summary">Pendant l\'enregistrement, garder l\'écran allumé.</string>
|
||||
<string name="about_preference_title">À propos d\'OpenTracks</string>
|
||||
<string name="export_with_photos">avec les photos</string>
|
||||
<string name="export_without_photos">sans les photos</string>
|
||||
</resources>
|
||||
|
||||
@@ -262,7 +262,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Sensor de Bluetooth</string>
|
||||
<string name="settings_sensor_connected">Conectado a %1$s</string>
|
||||
<string name="settings_stats_coordinate">Latitude/lonxitude</string>
|
||||
<string name="settings_stats_grade_elevation">Pendente/elevación</string>
|
||||
<string name="settings_stats_rate_title">Indicador preferido</string>
|
||||
<string name="settings_stats_units_imperial">Imperiais (millas, pés)</string>
|
||||
<string name="settings_stats_units_metric">Métricas (km, m)</string>
|
||||
|
||||
@@ -262,7 +262,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Bluetooth सेंसर</string>
|
||||
<string name="settings_sensor_connected">%1$s से जुड़ा है</string>
|
||||
<string name="settings_stats_coordinate">अक्षांश/देशांतर</string>
|
||||
<string name="settings_stats_grade_elevation">ढलान/ऊंचाई</string>
|
||||
<string name="settings_stats_rate_title">पसंदीदा दर</string>
|
||||
<string name="settings_stats_units_imperial">इंपीरियल (मील, फुट)</string>
|
||||
<string name="settings_stats_units_metric">मेट्रिक (किमी, मी)</string>
|
||||
|
||||
@@ -263,7 +263,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Senzor Bluetootha</string>
|
||||
<string name="settings_sensor_connected">Povezan sa senzorom %1$s</string>
|
||||
<string name="settings_stats_coordinate">Geografska širina/duljina</string>
|
||||
<string name="settings_stats_grade_elevation">Nagib/elevacija</string>
|
||||
<string name="settings_stats_rate_title">Preferirana stopa</string>
|
||||
<string name="settings_stats_units_imperial">Imperijalne (milje, stope)</string>
|
||||
<string name="settings_stats_units_metric">Metričke (km, m)</string>
|
||||
|
||||
@@ -263,7 +263,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Bluetooth érzékelő</string>
|
||||
<string name="settings_sensor_connected">Kapcsolódva ehhez: %1$s</string>
|
||||
<string name="settings_stats_coordinate">Szélesség/hosszúság</string>
|
||||
<string name="settings_stats_grade_elevation">Lejtés/magasság</string>
|
||||
<string name="settings_stats_rate_title">Mérték választása</string>
|
||||
<string name="settings_stats_units_imperial">Angolszász (mérföld, láb)</string>
|
||||
<string name="settings_stats_units_metric">Metrikus (km, m)</string>
|
||||
|
||||
@@ -264,7 +264,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Sensor Bluetooth</string>
|
||||
<string name="settings_sensor_connected">Tersambung ke %1$s</string>
|
||||
<string name="settings_stats_coordinate">Lintang/Bujur</string>
|
||||
<string name="settings_stats_grade_elevation">Kemiringan/Tinggi</string>
|
||||
<string name="settings_stats_rate_title">Laju yang dipilih</string>
|
||||
<string name="settings_stats_units_imperial">Imperial (mil, kaki)</string>
|
||||
<string name="settings_stats_units_metric">Metrik (km, m)</string>
|
||||
|
||||
@@ -265,7 +265,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Sensore bluetooth</string>
|
||||
<string name="settings_sensor_connected">Connesso a %1$s</string>
|
||||
<string name="settings_stats_coordinate">Latitudine/longitudine</string>
|
||||
<string name="settings_stats_grade_elevation">Pendenza/altezza</string>
|
||||
<string name="settings_stats_rate_title">Velocità preferita</string>
|
||||
<string name="settings_stats_units_imperial">Imperiale (mi, ft)</string>
|
||||
<string name="settings_stats_units_metric">Metrico (km, m)</string>
|
||||
|
||||
@@ -264,7 +264,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">חיישן Bluetooth</string>
|
||||
<string name="settings_sensor_connected">מחובר ל-%1$s</string>
|
||||
<string name="settings_stats_coordinate">קו רוחב/קו אורך</string>
|
||||
<string name="settings_stats_grade_elevation">שיפוע/גובה</string>
|
||||
<string name="settings_stats_rate_title">קצב מועדף</string>
|
||||
<string name="settings_stats_units_imperial">אימפריאלי (מייל, רגל)</string>
|
||||
<string name="settings_stats_units_metric">מטרי (ק\"מ, מ\')</string>
|
||||
|
||||
@@ -265,7 +265,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Bluetoothのセンサー</string>
|
||||
<string name="settings_sensor_connected">接続先: %1$s</string>
|
||||
<string name="settings_stats_coordinate">緯度/経度</string>
|
||||
<string name="settings_stats_grade_elevation">傾斜/標高</string>
|
||||
<string name="settings_stats_rate_title">計測方法</string>
|
||||
<string name="settings_stats_units_imperial">ヤード・ポンド法(マイル、フィート)</string>
|
||||
<string name="settings_stats_units_metric">メートル法(km、m)</string>
|
||||
|
||||
@@ -264,7 +264,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">블루투스 센서</string>
|
||||
<string name="settings_sensor_connected">%1$s에 연결됨</string>
|
||||
<string name="settings_stats_coordinate">위도/경도</string>
|
||||
<string name="settings_stats_grade_elevation">등급/고도</string>
|
||||
<string name="settings_stats_rate_title">선호하는 속도</string>
|
||||
<string name="settings_stats_units_imperial">야드파운드법(마일, 피트)</string>
|
||||
<string name="settings_stats_units_metric">미터법(km,m)</string>
|
||||
|
||||
@@ -265,7 +265,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">„Bluetooth“ jutiklis</string>
|
||||
<string name="settings_sensor_connected">Prijungta prie %1$s</string>
|
||||
<string name="settings_stats_coordinate">Platuma ir ilguma</string>
|
||||
<string name="settings_stats_grade_elevation">Laipsnis / Aukštis</string>
|
||||
<string name="settings_stats_rate_title">Pageidaujamas greitis</string>
|
||||
<string name="settings_stats_units_imperial">Imperinės (myl., pėdos)</string>
|
||||
<string name="settings_stats_units_metric">Metrinės (km, m)</string>
|
||||
|
||||
@@ -262,7 +262,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Bluetooth sensors</string>
|
||||
<string name="settings_sensor_connected">Savienots ar %1$s</string>
|
||||
<string name="settings_stats_coordinate">Platums/garums</string>
|
||||
<string name="settings_stats_grade_elevation">Slīpums/pacēlums</string>
|
||||
<string name="settings_stats_rate_title">Vēlamais rādītājs</string>
|
||||
<string name="settings_stats_units_imperial">Standarta (jūdzes, pēdas)</string>
|
||||
<string name="settings_stats_units_metric">Metriskās (km, m)</string>
|
||||
|
||||
@@ -263,7 +263,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Senzor Bluetooth</string>
|
||||
<string name="settings_sensor_connected">Conectat la %1$s</string>
|
||||
<string name="settings_stats_coordinate">Latitudine/longitudine</string>
|
||||
<string name="settings_stats_grade_elevation">Înclinare/Elevație</string>
|
||||
<string name="settings_stats_rate_title">Rata de preferat</string>
|
||||
<string name="settings_stats_units_imperial">Imperial (mi, ft)</string>
|
||||
<string name="settings_stats_units_metric">Metric (km, m)</string>
|
||||
|
||||
@@ -263,7 +263,6 @@
|
||||
<string name="settings_sensor_bluetooth_sensor">Sensor Bluetooth</string>
|
||||
<string name="settings_sensor_connected">Disambungkan ke %1$s</string>
|
||||
<string name="settings_stats_coordinate">Latitud/Longitud</string>
|
||||
<string name="settings_stats_grade_elevation">Cerun/Ketinggian</string>
|
||||
<string name="settings_stats_rate_title">Kadar pilihan</string>
|
||||
<string name="settings_stats_units_imperial">Imperial (bt, kaki)</string>
|
||||
<string name="settings_stats_units_metric">Metrik (km, m)</string>
|
||||
|
||||