Compare commits

...

33 Commits

Author SHA1 Message Date
Dennis Guse 306522863a Release v3.5.0 2020-03-21 23:18:57 +01:00
Dennis Guse e54d85e04d ChartFragment: unregister from preference changes on pause. 2020-03-21 20:54:49 +01:00
Dennis Guse cb30ad27c3 Removed that tracks store count of the locations. 2020-03-20 19:44:31 +01:00
Dennis Guse 8685c9cc88 Merge pull request #162 from rgmf/issue#89
Share pictures from marker.
2020-03-18 22:08:42 +01:00
Román a6f03d0f23 Share pictures from marker.
Fixes #89.
2020-03-18 19:33:57 +01:00
Dennis Guse e3c812b155 TrackDeleteActivity now has a transparant background. 2020-03-18 07:43:21 +01:00
Dennis Guse f149de9ce3 Merge pull request #167 from rgmf/issue#163
Add android:key in PreferenceCategory with expandable.
2020-03-17 21:54:08 +01:00
Dennis Guse 3447dd2569 Merge pull request #165 from OpenTracksApp/dbCleanup
Database cleanup and optimization
2020-03-17 21:44:28 +01:00
Dennis Guse b2b8c7fd54 Database upgrade (incl. tests): removed unused columns and added indices.
Fixes #108.
2020-03-17 21:39:10 +01:00
Román 486bcd0aec Add android:key in PreferenceCategory with expandable.
Fixes #163.
2020-03-17 10:57:04 +01:00
Dennis Guse c8a33201f5 Moved annoucement text generation into util class. 2020-03-16 21:43:23 +01:00
Dennis Guse 4a37c2cc7e Cleanup. 2020-03-16 21:43:23 +01:00
Dennis Guse d23db5bafd Charts use preferred units and rate.
Fixes #150.
2020-03-16 21:43:23 +01:00
Dennis Guse ac528f9a10 Use trackname for sharing files.
Fixes #130.
2020-03-16 21:43:23 +01:00
Dennis Guse 256eadd49d Added some french translation provided by @edern-appere.
Fixes #147.
2020-03-16 21:43:23 +01:00
Dennis Guse 64e58934a5 Settings: fix label for elevation (removed "Grade" as it is not shown anymore). 2020-03-16 21:43:23 +01:00
Dennis Guse 92dfadfdba Remove elevation min/max from UI as GPS-derived data often lead to invalid data (especially initial fix phase).
Fixes #148.
2020-03-16 21:43:23 +01:00
Román cbce70143b All marker management is now in MarkerEditActivity (including take photo and added gallery image to the marker).
Issue 122.
2020-03-11 19:59:15 +01:00
Román f4b9e3159e New screenshots for F-Droid/PlayStore. Fixes #132. 2020-03-11 19:40:57 +01:00
Dennis Guse 928781391d Release v3.4.0 2020-02-05 23:41:55 +01:00
Dennis Guse 05cd672464 Updated help text. 2020-02-05 23:41:50 +01:00
Dennis Guse d3109bbc37 Cleanup. 2020-02-05 23:08:17 +01:00
Dennis Guse eabdb7c721 Unify order of show on map and share menu entries. 2020-02-04 19:52:05 +01:00
Peter Storch ad990e8cd7 Forward multiple tracks to dashboard 2020-02-04 00:07:17 +01:00
Dennis Guse db3eef0104 Test: date comparison tends to be flaky. 2020-02-03 23:27:19 +01:00
Dennis Guse c4f1ba7962 Upgrade material design dependency.
Requires including androidx.documentfile directly.
2020-02-03 23:27:19 +01:00
Dennis Guse 7451fbd413 CI: enable testing via Github Actions. 2020-02-03 23:27:19 +01:00
Dennis Guse b6c55de55a CI: enable testing TravisCI on again. 2020-02-03 23:27:19 +01:00
Dennis Guse dd3ee6d99e Bugfix: match internal ContentURI correctly.
TrackPoint by TrackId was not working (and not used).
2020-02-03 21:32:06 +01:00
Dennis Guse bbb6a7c22c Bugfix: keep TrackRecordingService in foreground if paused. 2020-02-03 20:45:14 +01:00
Dennis Guse dc1b50a7bf Cleanup: move PAUSE_LATITUDE and RESUME_LATITUDE to TrackPointsColumn. 2020-02-03 20:45:14 +01:00
Dennis Guse 21bfb88e0b Bugfix: initialize fragments from bundle rather via constructor.
Introduced in 8b11e1d611
This fixes #127.
2020-02-03 19:51:04 +01:00
Dennis Guse aaab8adce9 ExportAsyncTask: always get a wakelock. 2020-02-03 19:51:04 +01:00
126 changed files with 1282 additions and 933 deletions
+21
View File
@@ -0,0 +1,21 @@
#https://github.com/marketplace/actions/android-emulator-runner
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: macos-latest
strategy:
matrix:
api-level: [21, 29]
target: [default]
steps:
- name: checkout
uses: actions/checkout@v2
- name: run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: x86_64
script: ./gradlew connectedCheck
-5
View File
@@ -45,19 +45,16 @@ captures/
# IntelliJ IDEA
*.iml
*.iws
/out/
# User-specific configurations
.idea/.name
.idea/assetWizardSettings.xml
.idea/caches/
.idea/compiler.xml
.idea/copyright/profiles_settings.xml
.idea/dataSources.ids
.idea/datasources.xml
.idea/dictionaries
.idea/dynamic.xml
.idea/encodings.xml
.idea/jsLibraryMappings.xml
@@ -67,10 +64,8 @@ captures/
.idea/scopes/scope_settings.xml
.idea/shelf/
.idea/sqlDataSources.xml
.idea/tasks.xml
.idea/vcs.xml
.idea/uiDesigner.xml
.idea/workspace.xml
# Keystore files
*.jks
+10 -10
View File
@@ -22,18 +22,18 @@ android:
- android-29
# Emulator Management: Create, Start and Wait
#install:
# - echo y | sdkmanager "platform-tools"
# - echo y | sdkmanager --channel=4 "emulator"
# - echo y | sdkmanager "system-images;android-$API;$EMU_FLAVOR;$ABI"
# - echo no | avdmanager create avd --force -n test -k "system-images;android-$API;$EMU_FLAVOR;$ABI" -c 10M
# - emulator -verbose -avd test -no-accel -no-snapshot -no-window -camera-back none -camera-front none -selinux permissive -qemu -m 2048 &
# - android-wait-for-emulator
# - adb shell input keyevent 82 &
install:
- echo y | sdkmanager "platform-tools"
- echo y | sdkmanager --channel=4 "emulator"
- echo y | sdkmanager "system-images;android-$API;$EMU_FLAVOR;$ABI"
- echo no | avdmanager create avd --force -n test -k "system-images;android-$API;$EMU_FLAVOR;$ABI" -c 10M
- emulator -verbose -avd test -no-accel -no-snapshot -no-window -camera-back none -camera-front none -selinux permissive -qemu -m 2048 &
- android-wait-for-emulator
- adb shell input keyevent 82 &
script:
- ./gradlew clean build
# - ./gradlew connectedCheck
- ./gradlew build
- ./gradlew connectedCheck
sbefore_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
+3 -3
View File
@@ -14,15 +14,15 @@ For the time being, the OpenTracks Data API can be used to implement mainly cust
## Implementation
OpenTracks by default does not expose any data to 3rd-party applications.
On the request of the user (i.e., pressing a button), an Intent is created that exposes the `Track` data (it's summary) as well as the `TrackPoint`s.
On the request of the user (i.e., pressing a button), an Intent is created that exposes the `Track` data (it's summary) as well as the `TrackPoint`s of one or more selected tracks.
For each, one URI is provided.
These URIs notify observers if on content changes (e.g., a new `TrackPoint` was added).
Limitations:
* only tracks selected by the user is exposed,
* only tracks selected by the user are exposed,
* `Waypoints` are not exposed,
* access is only granted temporarily and automatically revoked (see `FLAG_GRANT_READ_URI_PERMISSION`),
* no write access is impossible, and
* no write access is possible, and
* neither track recordings can be started, paused, or stopped.
All required information can be found in `IntentDashboardUtils.java`.
+2 -1
View File
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

@@ -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);
}
/**
+5 -3
View File
@@ -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>
+2 -1
View File
@@ -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" />
+30 -70
View File
@@ -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>
+33
View File
@@ -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>
+11 -90
View File
@@ -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>
+5 -5
View File
@@ -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"
+5
View File
@@ -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"
+17
View File
@@ -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>
-6
View File
@@ -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"
-1
View File
@@ -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>
-1
View File
@@ -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>
+4 -1
View File
@@ -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>
-1
View File
@@ -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>
+3 -1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
+5 -1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
+7 -1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>

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