Compare commits

..

26 Commits

Author SHA1 Message Date
Dennis Guse c0672c41df Bugfix: crash if heart rate sensor was null (i.e., preference not initialized). 2019-11-29 23:18:49 +01:00
Dennis Guse 4c6601fbc5 Release v3.2.8 2019-11-29 22:58:22 +01:00
edern-appere b99c87d1bd Added French translation for Fdroid. 2019-11-29 22:46:43 +01:00
Dennis Guse 66ffbfe951 Fixed ChartFragment tests.
Initialization of looper was missing.
2019-11-29 22:43:51 +01:00
Dennis Guse 8cb01a1027 Bugfix: sharing a track was not adding the file(s).
While migrating to enable sharing multiple tracks a configuration option was missed.
2019-11-29 22:39:36 +01:00
Dennis Guse 193e5eccbb Cleanup. 2019-11-29 22:39:36 +01:00
Dennis Guse bf7fc28bb0 Bugfix: picture name suffixes were malformed on export and thus could not be re-imported. 2019-11-29 22:39:31 +01:00
Dennis Guse e35cbc343a Add pull request template. 2019-11-29 07:54:39 +01:00
Dennis Guse 1c7b053c8c Fixed MarkerDetailActivity.
The orientation of the layout was not set and thus adding the toolbar broke the UI.
2019-11-28 18:09:11 +01:00
Dennis Guse 88cdcd4b47 Use GestureDetector rather than doing it on our own. 2019-11-27 21:42:05 +01:00
Dennis Guse 1432fed8f9 Removed zoom controls for ChartFragment. 2019-11-27 21:41:53 +01:00
Dennis Guse a14307b9a4 Moved Chart* into own package. 2019-11-27 20:19:12 +01:00
Dennis Guse 3bdce5aa08 Cleanup: use autocloseable. 2019-11-25 23:09:08 +01:00
Dennis Guse d7526f93b8 Cleanup: removed ContentProviderUtils interface. 2019-11-25 23:02:48 +01:00
Dennis Guse 8932578b90 Release v3.2.7 2019-11-25 22:03:40 +01:00
Dennis Guse 631ac9881d Wrap help into PreferenceCategory. 2019-11-25 22:03:02 +01:00
Dennis Guse 78334ed797 Fixed Spanish project description (short); was still German. 2019-11-24 21:12:07 +01:00
Dennis Guse b06da89654 Show chart for distance as well as time.
Also removed configuration option.
2019-11-24 19:55:00 +01:00
Dennis Guse d6c2e78993 Cleanup. 2019-11-24 18:48:31 +01:00
Dennis Guse 67d1ac551d Show GPS accuracy while recording in the notification of the TrackRecordingService and notify if accuracy goes beyond configured threshold. 2019-11-24 18:44:04 +01:00
Dennis Guse dcb6be717d Cleanup. 2019-11-23 22:46:39 +01:00
Dennis Guse f2c04eec1a Add help activity.
Can be accessed from Settings.
2019-11-23 21:43:07 +01:00
Dennis Guse a08311cf43 Temporary bugfix: disable landscape mode for TrackListActivity. 2019-11-22 07:44:01 +01:00
Dennis Guse 1e28f3aa86 Add TTSUtils.
(required by previous commit)
2019-11-22 07:43:19 +01:00
Dennis Guse 426af41f2c Move TTS stream definition into one place. 2019-11-20 19:27:41 +01:00
Dennis Guse fd08a219f9 Add Spanish store description. 2019-11-20 19:13:21 +01:00
71 changed files with 1907 additions and 1617 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: Bug report
about: Report issues of Opentracks features
about: Report issues of OpenTracks features
title: ''
labels: potential bug
assignees: ''
+10
View File
@@ -0,0 +1,10 @@
# Thanks for your contribution.
**Describe the pull request**
A clear and concise description of what the pull request changes/adds.
**Link to the the issue**
(If available): The link to the issue that this pull request solves.
**License agreement**
By opening this pull request, you are providing your contribution under the _Apache License 2.0_ (see [LICENSE.md](LICENSE.md)).
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RenderSettings">
<option name="showDecorations" value="true" />
</component>
</project>
@@ -1,4 +1,4 @@
Open Tracks – ein GPS-Tracker, der deine Daten für sich behält.
OpenTracks – ein GPS-Tracker, der deine Daten für sich behält.
Erfasst deine Kilometer, ohne dass es jemand anderes tut.
Funktionen & Features:
@@ -1 +1 @@
Open Tracks – ein GPS-Tracker, der deine Daten für sich behält.
OpenTracks – ein GPS-Tracker, der deine Daten für sich behält.
@@ -0,0 +1,10 @@
v3.2.7: OpenTracks
Changes:
- Notification show GPS accuracy
- Show heads-up notification if GPS accuracy drops beyond threshold
- Always show chart by time as well as by distance
- Added help (in Settings)
Bugfix:
- TrackDetailActivity: disabled non-working landscape mode (temporary)
@@ -0,0 +1,10 @@
v3.2.8: OpenTracks
Changes:
- Chart: zoom via gestures
Bugfixes:
- Export via KMZ did not export picture filenames correctly
- Sharing multiple tracks did not include KMZ files
- MarkerDetailActivity's UI was not shown
- Settings crashed if preference of heart rate was not initialized
@@ -0,0 +1,8 @@
OpenTracks es un tracker deportivo que respeta tu privacidad.
Funciones:
* Tracker deportivo: haz un seguimiento de tus actividades al aire libre
* Marca lugares de interés, incluso agrega fotos a los marcadores
* Pulsómetro (compatible con dispositivos Bluetooth LE)
* Exporta tus rutas como KML o GPX
* Completamente offline, no necesitas internet
@@ -0,0 +1 @@
OpenTracks es un tracker deportivo que respeta tu privacidad.
@@ -0,0 +1 @@
OpenTracks
@@ -0,0 +1,9 @@
OpenTracks est une application de suivi d'activités sportives qui respecte totalement votre vie privée.
Fonctionnalités :
* Suivi d'activité : enregistre vos activités sportives et de plein air
* Photos et repères : ajout de photos et de repères pendant l'enregistrement de l'activité
* Rythme cardiaque : support des capteurs Bluetooth LE
* Export : export des traces au format KML ou GPX
* Pas d'accès internet requis : Internet n'est pas nécessaire pour le fonctionnement de l'application
@@ -0,0 +1 @@
OpenTracks est une application de suivi d'activités sportives qui respecte totalement votre vie privée.
@@ -13,7 +13,7 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks;
package de.dennisguse.opentracks.chart;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -23,6 +23,8 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
/**
* Tests {@link ChartValueSeries}.
*
@@ -35,21 +35,19 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationFactory;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.stats.TripStatistics;
import static org.mockito.Mockito.when;
/**
* A unit test for {@link ContentProviderUtilsImpl}.
* A unit test for {@link ContentProviderUtils}.
*
* @author Bartlomiej Niechwiej
* @author Youtao Liu
*/
@RunWith(MockitoJUnitRunner.class)
public class CustomContentProviderUtilsImplTest {
public class CustomContentProviderUtilsTest {
private static final String NAME_PREFIX = "test name";
private static final String MOCK_DESC = "Mock Next Waypoint Desc!";
private static final String TEST_DESC = "Test Desc!";
@@ -69,13 +67,13 @@ public class CustomContentProviderUtilsImplTest {
@Before
public void setUp() {
providerUtils = ContentProviderUtils.Factory.get(context);
providerUtils = new ContentProviderUtils(context);
providerUtils.deleteAllTracks(context);
}
@Test
public void testLocationIterator_noPoints() {
testIterator(1, 0, 1, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(1, 0, 1, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
@@ -105,36 +103,36 @@ public class CustomContentProviderUtilsImplTest {
@Test
public void testLocationIterator_noBatchAscending() {
testIterator(1, 50, 100, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 100, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_noBatchDescending() {
testIterator(1, 50, 100, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 100, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_batchAscending() {
testIterator(1, 50, 11, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 11, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_batchDescending() {
testIterator(1, 50, 11, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(1, 50, 11, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
}
@Test
public void testLocationIterator_largeTrack() {
testIterator(1, 20000, 2000, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
testIterator(1, 20000, 2000, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
}
private List<Location> testIterator(long trackId, int numPoints, int batchSize, boolean descending, LocationFactory locationFactory) {
long lastPointId = initializeTrack(trackId, numPoints);
((ContentProviderUtilsImpl) providerUtils).setDefaultCursorBatchSize(batchSize);
((ContentProviderUtils) providerUtils).setDefaultCursorBatchSize(batchSize);
List<Location> locations = new ArrayList<Location>(numPoints);
try (LocationIterator it = providerUtils.getTrackPointLocationIterator(trackId, -1L, descending, locationFactory)) {
while (it.hasNext()) {
@@ -173,7 +171,7 @@ public class CustomContentProviderUtilsImplTest {
// Load all inserted locations.
long lastPointId = -1;
int counter = 0;
try (LocationIterator it = providerUtils.getTrackPointLocationIterator(id, -1L, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
try (LocationIterator it = providerUtils.getTrackPointLocationIterator(id, -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
while (it.hasNext()) {
it.next();
lastPointId = it.getLocationId();
@@ -189,7 +187,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#createTrack(Cursor)}.
* Tests the method {@link ContentProviderUtils#createTrack(Cursor)}.
*/
@Test
public void testCreateTrack() {
@@ -216,7 +214,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#deleteAllTracks(Context)}
* Tests the method {@link ContentProviderUtils#deleteAllTracks(Context)}
*/
@Test
public void testDeleteAllTracks() {
@@ -245,7 +243,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#deleteTrack(Context, long)}.
* Tests the method {@link ContentProviderUtils#deleteTrack(Context, long)}.
*/
@Test
public void testDeleteTrack() {
@@ -280,7 +278,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getAllTracks()}
* Tests the method {@link ContentProviderUtils#getAllTracks()}
*/
@Test
public void testGetAllTracks() {
@@ -293,7 +291,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getLastTrack()}
* Tests the method {@link ContentProviderUtils#getLastTrack()}
*/
@Test
public void testGetLastTrack() {
@@ -303,7 +301,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getTrack(long)}
* Tests the method {@link ContentProviderUtils#getTrack(long)}
*/
@Test
public void testGetTrack() {
@@ -313,7 +311,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#updateTrack(Track)}
* Tests the method {@link ContentProviderUtils#updateTrack(Track)}
*/
@Test
public void testUpdateTrack() {
@@ -330,7 +328,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#createContentValues(Waypoint)}.
* Tests the method {@link ContentProviderUtils#createContentValues(Waypoint)}.
*/
@Test
public void testCreateContentValues_waypoint() {
@@ -368,7 +366,7 @@ public class CustomContentProviderUtilsImplTest {
waypoint.setLocation(location);
providerUtils.insertWaypoint(waypoint);
ContentProviderUtilsImpl contentProviderUtils = new ContentProviderUtilsImpl(contentResolverMock);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(contentResolverMock);
long waypointId = System.currentTimeMillis();
waypoint.setId(waypointId);
@@ -381,7 +379,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#createWaypoint(Cursor)}.
* Tests the method {@link ContentProviderUtils#createWaypoint(Cursor)}.
*/
@Test
public void testCreateWaypoint() {
@@ -416,7 +414,7 @@ public class CustomContentProviderUtilsImplTest {
/**
* Tests the method
* {@link ContentProviderUtilsImpl#deleteWaypoint(Context, long, DescriptionGenerator)}
* {@link ContentProviderUtils#deleteWaypoint(Context, long, DescriptionGenerator)}
* when there is only one waypoint in the track.
*/
@Test
@@ -437,7 +435,7 @@ public class CustomContentProviderUtilsImplTest {
@Override
public String generateWaypointDescription(TripStatistics tripStatistics) {
return CustomContentProviderUtilsImplTest.MOCK_DESC;
return CustomContentProviderUtilsTest.MOCK_DESC;
}
@Override
@@ -452,7 +450,7 @@ public class CustomContentProviderUtilsImplTest {
/**
* Tests the method
* {@link ContentProviderUtilsImpl#deleteWaypoint(Context, long, DescriptionGenerator)}
* {@link ContentProviderUtils#deleteWaypoint(Context, long, DescriptionGenerator)}
* when there is more than one waypoint in the track.
*/
@Test
@@ -495,7 +493,7 @@ public class CustomContentProviderUtilsImplTest {
DescriptionGenerator descriptionGenerator = new DescriptionGenerator() {
@Override
public String generateWaypointDescription(TripStatistics tripStatistics) {
return CustomContentProviderUtilsImplTest.MOCK_DESC;
return CustomContentProviderUtilsTest.MOCK_DESC;
}
@Override
@@ -508,12 +506,12 @@ public class CustomContentProviderUtilsImplTest {
providerUtils.deleteWaypoint(context, waypoint1Id, descriptionGenerator);
Assert.assertNull(providerUtils.getWaypoint(waypoint1Id));
Assert.assertEquals(CustomContentProviderUtilsImplTest.MOCK_DESC, providerUtils.getWaypoint(waypoint2Id)
Assert.assertEquals(CustomContentProviderUtilsTest.MOCK_DESC, providerUtils.getWaypoint(waypoint2Id)
.getDescription());
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getFirstWaypointId(long)}.
* Tests the method {@link ContentProviderUtils#getFirstWaypointId(long)}.
*/
@Test
public void testGetFirstWaypointId() {
@@ -535,7 +533,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getNextWaypointNumber(long, WaypointType)}.
* Tests the method {@link ContentProviderUtils#getNextWaypointNumber(long, WaypointType)}.
*/
@Test
public void testGetNextWaypointNumber() {
@@ -594,8 +592,8 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#insertWaypoint(Waypoint)} and
* {@link ContentProviderUtilsImpl#getWaypoint(long)}.
* Tests the method {@link ContentProviderUtils#insertWaypoint(Waypoint)} and
* {@link ContentProviderUtils#getWaypoint(long)}.
*/
@Test
public void testInsertAndGetWaypoint() {
@@ -612,7 +610,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#updateWaypoint(Waypoint)}.
* Tests the method {@link ContentProviderUtils#updateWaypoint(Waypoint)}.
*/
@Test
public void testUpdateWaypoint() {
@@ -634,7 +632,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#bulkInsertTrackPoint(Location[], int, long)}.
* Tests the method {@link ContentProviderUtils#bulkInsertTrackPoint(Location[], int, long)}.
*/
@Test
public void testBulkInsertTrackPoint() {
@@ -650,7 +648,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#createTrackPoint(Cursor)}.
* Tests the method {@link ContentProviderUtils#createTrackPoint(Cursor)}.
*/
@Test
public void testCreateTrackPoint() {
@@ -710,7 +708,7 @@ public class CustomContentProviderUtilsImplTest {
/**
* Tests the method
* {@link ContentProviderUtilsImpl#insertTrackPoint(Location, long)}.
* {@link ContentProviderUtils#insertTrackPoint(Location, long)}.
*/
@Test
public void testInsertTrackPoint() {
@@ -724,7 +722,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getFirstTrackPointId(long)}.
* Tests the method {@link ContentProviderUtils#getFirstTrackPointId(long)}.
*/
@Test
public void testGetFirstTrackPointId() {
@@ -737,7 +735,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getLastTrackPointId(long)}.
* Tests the method {@link ContentProviderUtils#getLastTrackPointId(long)}.
*/
@Test
public void testGetLastTrackPointId() {
@@ -750,7 +748,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getLastValidTrackPoint(long)}.
* Tests the method {@link ContentProviderUtils#getLastValidTrackPoint(long)}.
*/
@Test
public void testGetLastValidTrackPoint() {
@@ -764,7 +762,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)} in descending.
* Tests the method {@link ContentProviderUtils#getTrackPointCursor(long, long, int, boolean)} in descending.
*/
@Test
public void testGetTrackPointCursor_desc() {
@@ -783,7 +781,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)} in ascending.
* Tests the method {@link ContentProviderUtils#getTrackPointCursor(long, long, int, boolean)} in ascending.
*/
@Test
public void testGetTrackPointCursor_asc() {
@@ -802,7 +800,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in descending.
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in descending.
*/
@Test
public void testGetTrackPointLocationIterator_desc() {
@@ -818,7 +816,7 @@ public class CustomContentProviderUtilsImplTest {
long startTrackPointId = trackpointIds[9];
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next();
@@ -829,7 +827,7 @@ public class CustomContentProviderUtilsImplTest {
}
/**
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in ascending.
* Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in ascending.
*/
@Test
public void testGetTrackPointLocationIterator_asc() {
@@ -845,7 +843,7 @@ public class CustomContentProviderUtilsImplTest {
long startTrackPointId = trackpointIds[0];
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY);
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next();
@@ -892,7 +890,7 @@ public class CustomContentProviderUtilsImplTest {
/**
* Checks the value of a location.
*
* @param i the index of this location which created in the method {@link CustomContentProviderUtilsImplTest#getTrack(long, int)}
* @param i the index of this location which created in the method {@link CustomContentProviderUtilsTest#getTrack(long, int)}
* @param location the location to be checked
*/
private void checkLocation(int i, Location location) {
@@ -58,7 +58,7 @@ public class SearchEngineTest {
@Before
public void setUp() {
providerUtils = ContentProviderUtils.Factory.get(context);
providerUtils = new ContentProviderUtils(context);
engine = new SearchEngine(providerUtils);
}
@@ -17,17 +17,20 @@
package de.dennisguse.opentracks.fragments;
import android.location.Location;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.ChartView;
import de.dennisguse.opentracks.TrackStubUtils;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -44,10 +47,23 @@ public class ChartFragmentTest {
private ChartFragment chartFragment;
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
@Before
public void setUp() {
chartFragment = new ChartFragment();
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext()));
boolean chartByDistance = false;
chartFragment = new ChartFragment(chartByDistance) {
};
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
chartFragment.setTripStatisticsUpdater(TrackStubUtils.INITIAL_TIME);
}
@@ -0,0 +1,54 @@
package de.dennisguse.opentracks.services;
import android.app.NotificationManager;
import android.content.Context;
import android.location.Location;
import androidx.core.app.NotificationCompat;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class TrackRecordingServiceNotificationManagerTest {
private Context context = ApplicationProvider.getApplicationContext();
@Mock
private Location locationMock;
@Mock
private NotificationCompat.Builder notificationCompatBuilder;
@Mock
private NotificationManager notificationManager;
@Test
public void updateLocation_triggersAlertOnlyOnFirstInaccurateLocation() {
when(locationMock.hasAccuracy()).thenReturn(true);
when(locationMock.getAccuracy()).thenReturn(999f);
when(notificationCompatBuilder.setContentText(anyString())).thenReturn(notificationCompatBuilder);
when(notificationCompatBuilder.setOnlyAlertOnce(anyBoolean())).thenReturn(notificationCompatBuilder);
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
// when
subject.updateLocation(context, locationMock, 100);
subject.updateLocation(context, locationMock, 100);
subject.updateLocation(context, locationMock, 1000);
subject.updateLocation(context, locationMock, 100);
// then
verify(notificationCompatBuilder, times(6)).setOnlyAlertOnce(true);
verify(notificationCompatBuilder, times(2)).setOnlyAlertOnce(false);
}
}
@@ -89,7 +89,7 @@ public class TrackRecordingServiceTest {
};
customContentProvider.attachInfo(context, null);
providerUtils = ContentProviderUtils.Factory.get(context);
providerUtils = new ContentProviderUtils(context);
// Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
@@ -68,7 +68,7 @@ public class TrackRecordingServiceTestLooper {
};
customContentProvider.attachInfo(context, null);
providerUtils = ContentProviderUtils.Factory.get(context);
providerUtils = new ContentProviderUtils(context);
// Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
@@ -60,10 +60,15 @@ public class FileUtilsTest extends TestCase {
*/
public void testSanitizeFileName() {
String name = "Swim\10ming-^across:/the/ pacific (ocean).";
String expected = "Swim_ming-^across_the_ pacific (ocean)_";
String expected = "Swim_ming-^across_the_ pacific (ocean).";
assertEquals(expected, FileUtils.sanitizeFileName(name));
}
public void testSanitizeFileNameWithSuffix() {
String name = "FileName.jpeg";
assertEquals(name, FileUtils.sanitizeFileName(name));
}
/**
* Tests {@link FileUtils#sanitizeFileName(String)} with i18n characters (in Chinese and Russian).
* Verifies that they are allowed.
+11 -5
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="3137"
android:versionName="v3.2.6"> <!-- For FDroid. -->
android:versionCode="3162"
android:versionName="v3.2.8"> <!-- For FDroid. -->
<!-- Permission to initialize services -->
<application
android:name=".Startup"
@@ -32,6 +32,10 @@ limitations under the License.
<activity android:name=".DeleteActivity" />
<activity
android:name=".HelpActivity"
android:label="@string/menu_help" />
<activity
android:name=".io.file.importer.ImportActivity"
android:theme="@style/ThemeCustomTransparent">
@@ -50,10 +54,10 @@ limitations under the License.
</intent-filter>
</activity>
<activity android:name=".MarkerEditActivity" />
<activity android:name=".MarkerDetailActivity" />
<activity android:name=".MarkerEditActivity" />
<activity
android:name=".MarkerListActivity"
android:label="@string/menu_markers">
@@ -79,7 +83,9 @@ limitations under the License.
<activity android:name=".TrackEditActivity" />
<activity android:name=".TrackListActivity">
<activity
android:name=".TrackListActivity"
android:screenOrientation="userPortrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -17,11 +17,12 @@
package de.dennisguse.opentracks;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import de.dennisguse.opentracks.util.TTSUtils;
/**
* @author Jimmy Shih
*/
@@ -32,7 +33,7 @@ public abstract class AbstractActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
// Set volume control stream for text to speech
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
setVolumeControlStream(TTSUtils.getTTSStream());
setContentView(getLayoutResId());
@@ -69,7 +69,7 @@ public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override
protected Boolean doInBackground(Void... params) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
if (trackIds.length == 1 && trackIds[0] == -1L) {
contentProviderUtils.deleteAllTracks(context);
@@ -0,0 +1,9 @@
package de.dennisguse.opentracks;
public class HelpActivity extends AbstractActivity {
@Override
protected int getLayoutResId() {
return R.layout.help;
}
}
@@ -61,7 +61,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
return;
}
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(this);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
Waypoint waypoint = contentProviderUtils.getWaypoint(markerId);
markerIds = new ArrayList<>();
@@ -127,7 +127,7 @@ public class MarkerEditActivity extends AbstractActivity {
if (newMarker) {
statisticsSection.setVisibility(View.GONE);
waypointSection.setVisibility(View.VISIBLE);
int nextWaypointNumber = trackId == -1L ? -1 : ContentProviderUtils.Factory.get(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
int nextWaypointNumber = trackId == -1L ? -1 : new ContentProviderUtils(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
if (nextWaypointNumber == -1) {
nextWaypointNumber = 0;
}
@@ -136,7 +136,7 @@ public class MarkerEditActivity extends AbstractActivity {
waypointMarkerType.setText("");
waypointDescription.setText("");
} else {
waypoint = ContentProviderUtils.Factory.get(this).getWaypoint(markerId);
waypoint = new ContentProviderUtils(this).getWaypoint(markerId);
if (waypoint == null) {
Log.d(TAG, "waypoint is null");
finish();
@@ -182,6 +182,6 @@ public class MarkerEditActivity extends AbstractActivity {
waypoint.setCategory(waypointMarkerType.getText().toString());
waypoint.setDescription(waypointDescription.getText().toString());
}
ContentProviderUtils.Factory.get(this).updateWaypoint(waypoint);
new ContentProviderUtils(this).updateWaypoint(waypoint);
}
}
@@ -130,7 +130,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
@@ -170,7 +170,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
searchEngine = new SearchEngine(contentProviderUtils);
@@ -45,7 +45,8 @@ import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.fragments.ChartFragment;
import de.dennisguse.opentracks.fragments.ChartDistanceFragment;
import de.dennisguse.opentracks.fragments.ChartTimeFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.StatsFragment;
@@ -188,7 +189,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
photoUri = savedInstanceState != null ? (Uri) savedInstanceState.getParcelable(PHOTO_URI_KEY) : null;
hasPhoto = savedInstanceState != null && savedInstanceState.getBoolean(HAS_PHOTO_KEY, false);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
contentProviderUtils = new ContentProviderUtils(this);
handleIntent(getIntent());
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
@@ -205,8 +206,11 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
TabSpec statsTabSpec = tabHost.newTabSpec(StatsFragment.STATS_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_stats_tab));
tabsAdapter.addTab(statsTabSpec, StatsFragment.class, null);
TabSpec chartTabSpec = tabHost.newTabSpec(ChartFragment.CHART_FRAGMENT_TAG).setIndicator(getString(R.string.track_detail_chart_tab));
tabsAdapter.addTab(chartTabSpec, ChartFragment.class, null);
TabSpec chartTimeTabSpec = tabHost.newTabSpec(ChartTimeFragment.CHART_FRAGMENT_TAG).setIndicator(getString(R.string.settings_chart_by_time));
tabsAdapter.addTab(chartTimeTabSpec, ChartTimeFragment.class, null);
TabSpec chartDistanceTabSpec = tabHost.newTabSpec(ChartDistanceFragment.CHART_FRAGMENT_TAG).setIndicator(getString(R.string.settings_chart_by_distance));
tabsAdapter.addTab(chartDistanceTabSpec, ChartDistanceFragment.class, null);
if (savedInstanceState != null) {
tabHost.setCurrentTabByTag(savedInstanceState.getString(CURRENT_TAB_TAG_KEY));
@@ -73,7 +73,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
return;
}
contentProviderUtils = ContentProviderUtils.Factory.get(this);
contentProviderUtils = new ContentProviderUtils(this);
track = contentProviderUtils.getTrack(trackId);
if (track == null) {
Log.e(TAG, "No track for " + trackId);
@@ -245,7 +245,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = ContentProviderUtils.Factory.get(this);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
@@ -14,7 +14,7 @@
* the License.
*/
package de.dennisguse.opentracks;
package de.dennisguse.opentracks.chart;
import android.content.Context;
import android.graphics.Canvas;
@@ -14,7 +14,7 @@
* the License.
*/
package de.dennisguse.opentracks;
package de.dennisguse.opentracks.chart;
import android.content.Context;
import android.content.Intent;
@@ -27,15 +27,19 @@ import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Scroller;
import androidx.core.view.GestureDetectorCompat;
import java.text.NumberFormat;
import java.util.ArrayList;
import de.dennisguse.opentracks.MarkerDetailActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
import de.dennisguse.opentracks.util.IntentUtils;
@@ -44,6 +48,7 @@ import de.dennisguse.opentracks.util.UnitConversions;
/**
* Visualization of the chart.
* Provides support for zooming (via pinch), scrolling, flinging, and selecting shown markers (single touch).
*
* @author Sandor Dornbush
* @author Leif Hendrik Wilden
@@ -91,8 +96,6 @@ public class ChartView extends View {
private final int markerHeight;
private final Scroller scroller;
private double maxX = 1.0;
private VelocityTracker velocityTracker = null;
private float lastMotionEventX = -1;
private int zoomLevel = 1;
private int leftBorder = BORDER;
@@ -107,18 +110,84 @@ public class ChartView extends View {
private int effectiveWidth = 0;
private int effectiveHeight = 0;
private boolean chartByDistance = true;
private boolean chartByDistance;
private boolean metricUnits = true;
private boolean reportSpeed = true;
private boolean showPointer = false;
/**
* Constructor.
*
* @param context the context
*/
public ChartView(Context context) {
private GestureDetectorCompat detectorScrollFlingTab = new GestureDetectorCompat(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (Math.abs(distanceX) > 0) {
int availableToScroll = effectiveWidth * (zoomLevel - 1) - getScrollX();
if (availableToScroll > 0) {
scrollBy(Math.min(availableToScroll, (int) distanceX));
}
}
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
fling((int) -velocityX);
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent event) {
// Check if the y event is within markerHeight of the marker center
if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2) < markerHeight) {
int minDistance = Integer.MAX_VALUE;
Waypoint nearestWaypoint = null;
synchronized (waypoints) {
for (Waypoint waypoint : waypoints) {
int distance = Math.abs(getX(getWaypointXValue(waypoint)) - (int) event.getX() - getScrollX());
if (distance < minDistance) {
minDistance = distance;
nearestWaypoint = waypoint;
}
}
}
if (nearestWaypoint != null && minDistance < markerWidth) {
Intent intent = IntentUtils.newIntent(getContext(), MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, nearestWaypoint.getId());
getContext().startActivity(intent);
return true;
}
}
return false;
}
});
private ScaleGestureDetector detectorZoom = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
if (scaleFactor >= 1.1f) {
zoomIn();
return true;
} else if (scaleFactor <= 0.9) {
zoomOut();
return true;
}
return false;
}
});
public ChartView(Context context, boolean chartByDistance) {
super(context);
this.chartByDistance = chartByDistance;
series[ELEVATION_SERIES] = new ChartValueSeries(context,
Integer.MIN_VALUE,
@@ -222,16 +291,6 @@ public class ChartView extends View {
series[index].setEnabled(enabled);
}
/**
* Sets chart by distance.
* It is expected that after changing this value, data will be reloaded.
*
* @param value true for by distance, false for by time
*/
public void setChartByDistance(boolean value) {
chartByDistance = value;
}
/**
* Sets metric units.
*
@@ -410,79 +469,14 @@ public class ChartView extends View {
@Override
public boolean onTouchEvent(MotionEvent event) {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain();
}
velocityTracker.addMovement(event);
float x = event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Stop the fling
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
lastMotionEventX = x;
break;
case MotionEvent.ACTION_MOVE:
if (lastMotionEventX == -1) {
break;
}
// Scroll to follow the motion event
int deltaX = (int) (lastMotionEventX - x);
lastMotionEventX = x;
if (deltaX < 0) {
if (getScrollX() > 0) {
scrollBy(deltaX);
}
} else if (deltaX > 0) {
int availableToScroll = effectiveWidth * (zoomLevel - 1) - getScrollX();
if (availableToScroll > 0) {
scrollBy(Math.min(availableToScroll, deltaX));
}
}
break;
case MotionEvent.ACTION_UP:
// Check if the y event is within markerHeight of the marker center
if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2) < markerHeight) {
int minDistance = Integer.MAX_VALUE;
Waypoint nearestWaypoint = null;
synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) {
Waypoint waypoint = waypoints.get(i);
int distance = Math.abs(getX(getWaypointXValue(waypoint)) - (int) event.getX() - getScrollX());
if (distance < minDistance) {
minDistance = distance;
nearestWaypoint = waypoint;
}
}
}
if (nearestWaypoint != null && minDistance < markerWidth) {
Intent intent = IntentUtils.newIntent(getContext(), MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, nearestWaypoint.getId());
getContext().startActivity(intent);
return true;
}
}
VelocityTracker myVelocityTracker = velocityTracker;
myVelocityTracker.computeCurrentVelocity(1000);
int initialVelocity = (int) myVelocityTracker.getXVelocity();
if (Math.abs(initialVelocity) > ViewConfiguration.getMinimumFlingVelocity()) {
fling(-initialVelocity);
}
if (velocityTracker != null) {
velocityTracker.recycle();
velocityTracker = null;
}
break;
}
return true;
boolean isZoom = detectorZoom.onTouchEvent(event);
boolean isScrollTab = detectorScrollFlingTab.onTouchEvent(event);
return isZoom || isScrollTab;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
updateEffectiveDimensionsIfChanged(
View.MeasureSpec.getSize(widthMeasureSpec), View.MeasureSpec.getSize(heightMeasureSpec));
updateEffectiveDimensionsIfChanged(View.MeasureSpec.getSize(widthMeasureSpec), View.MeasureSpec.getSize(heightMeasureSpec));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@@ -632,8 +626,7 @@ public class ChartView extends View {
private String getXAxisLabel() {
Context context = getContext();
if (chartByDistance) {
return metricUnits ? context.getString(R.string.unit_kilometer)
: context.getString(R.string.unit_mile);
return metricUnits ? context.getString(R.string.unit_kilometer) : context.getString(R.string.unit_mile);
} else {
return context.getString(R.string.description_time);
}
@@ -733,8 +726,7 @@ public class ChartView extends View {
* @param yValue the y value
* @return the marker width.
*/
private float drawYAxisMarker(
ChartValueSeries chartValueSeries, Canvas canvas, int xPosition, int yValue) {
private float drawYAxisMarker(ChartValueSeries chartValueSeries, Canvas canvas, int xPosition, int yValue) {
String marker = chartValueSeries.formatMarker(yValue);
Paint paint = chartValueSeries.getMarkerPaint();
Rect rect = getRect(paint, marker);
@@ -869,8 +861,7 @@ public class ChartView extends View {
topBorder = (int) (density * BORDER + titleDimensions[0] * (titleDimensions[1] + spacer));
Rect xAxisLabelRect = getRect(axisPaint, getXAxisLabel());
// border + x axis marker + spacer + .5 x axis label
bottomBorder = (int) (density * BORDER + getRect(xAxisMarkerPaint, "1").height() + spacer
+ (xAxisLabelRect.height() / 2));
bottomBorder = (int) (density * BORDER + getRect(xAxisMarkerPaint, "1").height() + spacer + (xAxisLabelRect.height() / 2));
rightBorder = (int) (density * BORDER + xAxisLabelRect.width() + spacer);
updateEffectiveDimensions();
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
package de.dennisguse.opentracks.content;
import android.location.Location;
import android.location.LocationManager;
/**
* Creates a new {@link SensorDataSetLocation}.
* An implementation can create new instances or reuse existing instances for optimization.
*/
public class LocationFactory {
/**
* The default {@link LocationFactory} which creates a location each time.
*/
public static LocationFactory DEFAULT_LOCATION_FACTORY = new LocationFactory();
public Location createLocation() {
return new SensorDataSetLocation(LocationManager.GPS_PROVIDER);
}
}
@@ -0,0 +1,22 @@
package de.dennisguse.opentracks.content;
import android.database.Cursor;
import android.location.Location;
import java.util.Iterator;
/**
* A lightweight wrapper around the original {@link Cursor} with a method to clean up.
*/
public interface LocationIterator extends Iterator<Location>, AutoCloseable {
/**
* Gets the most recently retrieved track point id by {@link #next()}.
*/
long getLocationId();
/**
* Closes the iterator.
*/
void close();
}
@@ -198,7 +198,7 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
ContentProviderUtils contentProviderUtils = new ContentProviderUtilsImpl(this);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
long[] trackIds = parseURI(uri);
final Track[] tracks = new Track[trackIds.length];
@@ -30,7 +30,6 @@ import java.util.EnumSet;
import java.util.Set;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -45,7 +44,7 @@ public class TrackDataHub implements DataSourceListener {
* Target number of track points displayed by the map overlay.
* We may display more than this number of points.
*/
static final int TARGET_DISPLAYED_TRACK_POINTS = 5000;
private static final int TARGET_DISPLAYED_TRACK_POINTS = 5000;
/**
* Maximum number of waypoints to displayed.
@@ -97,8 +96,7 @@ public class TrackDataHub implements DataSourceListener {
}
public synchronized static TrackDataHub newInstance(Context context) {
return new TrackDataHub(context, new TrackDataManager(), ContentProviderUtils.Factory.get(
context), TARGET_DISPLAYED_TRACK_POINTS);
return new TrackDataHub(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACK_POINTS);
}
public void start() {
@@ -448,7 +446,7 @@ public class TrackDataHub implements DataSourceListener {
int samplingFrequency = -1;
boolean includeNextPoint = false;
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
while (locationIterator.hasNext()) {
Location location = locationIterator.next();
@@ -0,0 +1,10 @@
package de.dennisguse.opentracks.fragments;
public class ChartDistanceFragment extends ChartFragment {
public static final String CHART_FRAGMENT_TAG = ChartDistanceFragment.class.getSimpleName();
public ChartDistanceFragment() {
super(true);
}
}
@@ -22,9 +22,9 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ZoomControls;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
@@ -32,9 +32,9 @@ import androidx.fragment.app.FragmentActivity;
import java.util.ArrayList;
import java.util.EnumSet;
import de.dennisguse.opentracks.ChartView;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.TrackDataHub;
@@ -54,9 +54,9 @@ import de.dennisguse.opentracks.util.UnitConversions;
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class ChartFragment extends Fragment implements TrackDataListener {
public abstract class ChartFragment extends Fragment implements TrackDataListener {
public static final String CHART_FRAGMENT_TAG = "chartFragment";
private static final String STATE_CHART_VIEW_BY_DISTANCE_KEY = "chartViewByDistance";
private final ArrayList<double[]> pendingPoints = new ArrayList<>();
@@ -71,15 +71,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
private int recordingDistanceInterval;
// Modes of operation
private boolean chartByDistance = true;
private boolean chartByDistance;
private boolean[] chartShow = new boolean[]{true, true, true, true, true, true};
// UI elements
private ChartView chartView;
private ZoomControls zoomControls;
/**
* A runnable that will enable/disable zoom controls and orange pointer as appropriate and redraw.
* A runnable that will set the orange pointer as appropriate and redraw.
*/
private final Runnable updateChart = new Runnable() {
@Override
@@ -88,48 +87,34 @@ public class ChartFragment extends Fragment implements TrackDataListener {
return;
}
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
chartView.setShowPointer(isSelectedTrackRecording());
chartView.invalidate();
}
};
public ChartFragment(boolean chartByDistance) {
this.chartByDistance = chartByDistance;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
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.
*/
chartView = new ChartView(getActivity());
// Create a chartView here to store data thus won't need to reload all the data on every onStart or onResume.
chartView = new ChartView(getActivity(), chartByDistance);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.chart, container, false);
zoomControls = view.findViewById(R.id.chart_zoom_controls);
zoomControls.setOnZoomInClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
zoomIn();
}
});
zoomControls.setOnZoomOutClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
zoomOut();
}
});
return view;
return inflater.inflate(R.layout.chart, container, false);
}
@Override
public void onStart() {
super.onStart();
ViewGroup layout = getActivity().findViewById(R.id.chart_view_layout);
ViewGroup layout = getView().findViewById(R.id.chart_view_layout);
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
layout.addView(chartView, layoutParams);
}
@@ -151,10 +136,24 @@ public class ChartFragment extends Fragment implements TrackDataListener {
@Override
public void onStop() {
super.onStop();
ViewGroup layout = getActivity().findViewById(R.id.chart_view_layout);
ViewGroup layout = getView().findViewById(R.id.chart_view_layout);
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()) {
@@ -304,12 +303,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
*/
private void checkChartSettings() {
boolean needUpdate = false;
if (chartByDistance != PreferencesUtils.isChartByDistance(getActivity())) {
chartByDistance = !chartByDistance;
chartView.setChartByDistance(chartByDistance);
reloadTrackDataHub();
needUpdate = true;
}
if (setSeriesEnabled(ChartView.ELEVATION_SERIES, PreferencesUtils.shouldChartShowElevation(getActivity()))) {
needUpdate = true;
}
@@ -390,18 +383,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
}
private void zoomIn() {
chartView.zoomIn();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
private void zoomOut() {
chartView.zoomOut();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
/**
* Runs a runnable on the UI thread if possible.
*
@@ -0,0 +1,10 @@
package de.dennisguse.opentracks.fragments;
public class ChartTimeFragment extends ChartFragment {
public static final String CHART_FRAGMENT_TAG = ChartTimeFragment.class.getSimpleName();
public ChartTimeFragment() {
super(false);
}
}
@@ -81,7 +81,7 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
new Thread(new Runnable() {
@Override
public void run() {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(fragmentActivity);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(fragmentActivity);
for (long markerId : markerIds) {
contentProviderUtils.deleteWaypoint(fragmentActivity,
markerId, new DescriptionGeneratorImpl(fragmentActivity));
@@ -110,7 +110,7 @@ public class MarkerDetailFragment extends Fragment {
getFragmentManager().popBackStack();
return;
}
contentProviderUtils = ContentProviderUtils.Factory.get(getActivity());
contentProviderUtils = new ContentProviderUtils(getActivity());
handler = new Handler();
setHasOptionsMenu(true);
}
@@ -173,10 +173,12 @@ public enum TrackFileFormat {
private static final String MIME_KML = "application/vnd.google-earth.kml+xml";
public TrackExporter newTrackExporter(Context context, Track[] tracks, TrackExporterListener trackExporterListener) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
TrackWriter trackWriter = this.newTrackWriter(context, tracks.length > 1);
return new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener);
private static TrackExporter newKmzTrackExporter(Context context, TrackWriter trackWriter, Track[] tracks, TrackExporterListener trackExporterListener, boolean exportPhotos) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
FileTrackExporter fileTrackExporter = new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener);
return new KmzTrackExporter(contentProviderUtils, fileTrackExporter, tracks, exportPhotos);
}
/**
@@ -184,12 +186,10 @@ public enum TrackFileFormat {
*/
public abstract String getMimeType();
private static TrackExporter newKmzTrackExporter(Context context, TrackWriter trackWriter, Track[] tracks, TrackExporterListener trackExporterListener, boolean exportPhotos) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
FileTrackExporter fileTrackExporter = new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener);
return new KmzTrackExporter(contentProviderUtils, fileTrackExporter, tracks, exportPhotos);
public TrackExporter newTrackExporter(Context context, Track[] tracks, TrackExporterListener trackExporterListener) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
TrackWriter trackWriter = this.newTrackWriter(context, tracks.length > 1);
return new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener);
}
/**
@@ -68,7 +68,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
this.trackFileFormat = trackFileFormat;
this.directory = directory;
context = exportActivity.getApplicationContext();
contentProviderUtils = ContentProviderUtils.Factory.get(context);
contentProviderUtils = new ContentProviderUtils(context);
completed = false;
processedTrackCount = 0;
@@ -26,7 +26,8 @@ import androidx.annotation.NonNull;
import java.io.OutputStream;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.content.LocationFactory;
import de.dennisguse.opentracks.content.LocationIterator;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
@@ -63,7 +64,7 @@ public class FileTrackExporter implements TrackExporter {
}
@Override
public boolean writeTrack(Context context, @NonNull OutputStream outputStream) {
public boolean writeTrack(@NonNull Context context, @NonNull OutputStream outputStream) {
try {
trackWriter.prepare(outputStream);
trackWriter.writeHeader(tracks);
@@ -209,7 +210,7 @@ public class FileTrackExporter implements TrackExporter {
*
* @author Jimmy Shih
*/
private class TrackWriterLocationFactory implements ContentProviderUtils.LocationFactory {
private class TrackWriterLocationFactory extends LocationFactory {
Location currentLocation;
Location lastLocation;
@@ -89,7 +89,7 @@ public class KmlTrackWriter implements TrackWriter {
this.exportSensorData = exportSensorData;
this.exportPhotos = exportPhotos;
this.descriptionGenerator = new DescriptionGeneratorImpl(context);
this.contentProviderUtils = ContentProviderUtils.Factory.get(context);
this.contentProviderUtils = new ContentProviderUtils(context);
}
@Override
@@ -19,7 +19,6 @@ package de.dennisguse.opentracks.io.file.exporter;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.OutputStream;
@@ -37,5 +36,5 @@ public interface TrackExporter {
* @param context the context
* @param outputStream the output stream
*/
boolean writeTrack(@Nullable Context context, @NonNull OutputStream outputStream);
boolean writeTrack(@NonNull Context context, @NonNull OutputStream outputStream);
}
@@ -39,8 +39,9 @@ import javax.xml.parsers.SAXParserFactory;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.content.DescriptionGeneratorImpl;
import de.dennisguse.opentracks.content.LocationFactory;
import de.dennisguse.opentracks.content.LocationIterator;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
@@ -165,7 +166,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
while (true) {
if (waypoint == null) {
@@ -55,7 +55,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
* @param context the context
*/
public GpxFileTrackImporter(Context context) {
this(context, ContentProviderUtils.Factory.get(context));
this(context, new ContentProviderUtils(context));
}
@VisibleForTesting
@@ -45,7 +45,9 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ImportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat;
private final String path;
private final Context context;
// TODO Can be removed, or?
private Context context;
// TODO Use weak reference
private ImportActivity importActivity;
private WakeLock wakeLock;
@@ -100,7 +102,7 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override
protected Boolean doInBackground(Void... params) {
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); // TODO Should be set to previous level at the end of this method.
// Get the wake lock if not recording or paused
boolean isRecording = PreferencesUtils.isRecording(importActivity);
boolean isPaused = PreferencesUtils.isRecordingTrackPaused(importActivity);
@@ -149,9 +151,11 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override
protected void onCancelled() {
context = null;
completed = true;
if (importActivity != null) {
importActivity.onAsyncTaskCompleted(importTrackCount, totalTrackCount);
importActivity = null;
}
}
@@ -163,17 +167,17 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private boolean importFile(final File file) {
TrackImporter trackImporter;
if (trackFileFormat == TrackFileFormat.GPX) {
trackImporter = new GpxFileTrackImporter(context);
trackImporter = new GpxFileTrackImporter(importActivity);
} else { //KML or KMZ
String extension = FileUtils.getExtension(file.getName());
if (TrackFileFormat.KML_ONLY_TRACK.getExtension().equals(extension)) {
trackImporter = new KmlFileTrackImporter(context, -1L);
trackImporter = new KmlFileTrackImporter(importActivity, -1L);
} else {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(importActivity);
Uri uri = contentProviderUtils.insertTrack(new Track());
long newId = Long.parseLong(uri.getLastPathSegment());
trackImporter = new KmzTrackImporter(context, newId);
trackImporter = new KmzTrackImporter(importActivity, newId);
}
}
@@ -81,7 +81,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
* @param importTrackId track id to import to. -1L to import to a new track.
*/
public KmlFileTrackImporter(Context context, long importTrackId) {
this(context, importTrackId, ContentProviderUtils.Factory.get(context));
this(context, importTrackId, new ContentProviderUtils(context));
}
@VisibleForTesting
@@ -72,7 +72,7 @@ public class KmzTrackImporter implements TrackImporter {
return -1L;
}
String fileName = zipEntry.getName();
if (fileName.equals(KmzTrackExporter.KMZ_KML_FILE)) {
if (KmzTrackExporter.KMZ_KML_FILE.equals(fileName)) {
trackId = parseKml(zipInputStream);
if (trackId == -1L) {
Log.d(TAG, "Unable to parse kml in kmz");
@@ -102,7 +102,7 @@ public class KmzTrackImporter implements TrackImporter {
*/
private void cleanImport(long trackId) {
if (PreferencesUtils.isRecording(trackId)) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
contentProviderUtils.deleteTrack(context, trackId);
}
@@ -125,15 +125,10 @@ public class KmzTrackImporter implements TrackImporter {
* @return the imported track id or -1L
*/
private long parseKml(ZipInputStream zipInputStream) throws IOException {
ByteArrayInputStream byteArrayInputStream = null;
try {
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, importTrackId);
byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream));
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, importTrackId);
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream))) {
return kmlFileTrackImporter.importFile(byteArrayInputStream);
} finally {
if (byteArrayInputStream != null) {
byteArrayInputStream.close();
}
}
}
@@ -160,12 +155,10 @@ public class KmzTrackImporter implements TrackImporter {
* @param fileName the file name
*/
private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException {
if (importTrackId == -1L) {
return;
}
if (fileName.equals("")) {
if (importTrackId == -1L || fileName.equals("")) {
return;
}
File dir = FileUtils.getPhotoDir(importTrackId);
FileUtils.ensureDirectoryExists(dir);
File file = new File(dir, fileName);
@@ -33,10 +33,6 @@ public class LocationManagerConnector {
private final Handler handler;
private final LocationManager locationManager;
private LocationListener requestLocationUpdates;
private float requestLocationUpdatesDistance;
private long requestLocationUpdatesTime;
public LocationManagerConnector(Context context, Looper looper) {
this.handler = new Handler(looper);
@@ -55,10 +51,7 @@ public class LocationManagerConnector {
@Override
@SuppressLint("MissingPermission")
public void run() {
requestLocationUpdatesTime = minTime;
requestLocationUpdatesDistance = minDistance;
requestLocationUpdates = locationListener;
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, requestLocationUpdatesTime, requestLocationUpdatesDistance, locationListener);
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, minTime, minDistance, locationListener);
}
});
}
@@ -70,6 +63,5 @@ public class LocationManagerConnector {
*/
public void removeLocationUpdates(final LocationListener locationListener) {
locationManager.removeUpdates(locationListener);
requestLocationUpdates = null;
}
}
@@ -16,9 +16,6 @@
package de.dennisguse.opentracks.services;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
@@ -30,7 +27,6 @@ import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -38,7 +34,6 @@ import android.os.PowerManager.WakeLock;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat;
import androidx.core.app.TaskStackBuilder;
import java.util.concurrent.ExecutorService;
@@ -48,9 +43,10 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.content.CustomContentProvider;
import de.dennisguse.opentracks.content.DescriptionGeneratorImpl;
import de.dennisguse.opentracks.content.LocationFactory;
import de.dennisguse.opentracks.content.LocationIterator;
import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
@@ -80,13 +76,13 @@ import de.dennisguse.opentracks.util.UnitConversions;
public class TrackRecordingService extends Service {
// The name of extra intent property to indicate whether we want to resume a previously recorded track.
public static final String RESUME_TRACK_EXTRA_NAME = "de.dennisguse.opentracks.RESUME_TRACK";
public static final String RESUME_TRACK_EXTRA_NAME = "RESUME_TRACK";
private static final int NOTIFICATION_ID = 123;
public static final double PAUSE_LATITUDE = 100.0;
public static final double RESUME_LATITUDE = 200.0;
// Anything faster than that (in meters per second) will be considered moving.
public static final double MAX_NO_MOVEMENT_SPEED = 0.224;
@VisibleForTesting
static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;
@@ -95,13 +91,13 @@ public class TrackRecordingService extends Service {
private static final long ONE_MINUTE = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
// The following variables are set in onCreate:
private ExecutorService executorService;
private Context context;
private ContentProviderUtils contentProviderUtils;
private Handler handler;
private LocationManagerConnector locationManagerConnector;
private PeriodicTaskExecutor voiceExecutor;
private PeriodicTaskExecutor splitExecutor;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceNotificationManager notificationManager;
private long recordingTrackId;
private boolean recordingTrackPaused;
private LocationListenerPolicy locationListenerPolicy;
@@ -117,10 +113,11 @@ public class TrackRecordingService extends Service {
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (PreferencesUtils.isKey(context, R.string.recording_track_id_key, key)) {
Context context = TrackRecordingService.this;
if (PreferencesUtils.isKey(TrackRecordingService.this, R.string.recording_track_id_key, key)) {
// Only through the TrackRecordingService can one stop a recording and set the recordingTrackId to -1L.
if (PreferencesUtils.isRecording(context)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(context);
if (PreferencesUtils.isRecording(TrackRecordingService.this)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(TrackRecordingService.this);
}
}
if (PreferencesUtils.isKey(context, R.string.recording_track_paused_key, key)) {
@@ -218,14 +215,14 @@ public class TrackRecordingService extends Service {
public void onCreate() {
super.onCreate();
executorService = Executors.newSingleThreadExecutor();
context = this;
contentProviderUtils = ContentProviderUtils.Factory.get(this);
contentProviderUtils = new ContentProviderUtils(this);
handler = new Handler();
locationManagerConnector = new LocationManagerConnector(this, handler.getLooper());
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory());
splitExecutor = new PeriodicTaskExecutor(this, new SplitPeriodicTaskFactory());
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
notificationManager = new TrackRecordingServiceNotificationManager(this);
// onSharedPreferenceChanged might not set recordingTrackId.
recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
@@ -267,7 +264,7 @@ public class TrackRecordingService extends Service {
}
// Reverse order from onCreate
showNotification(false);
showNotification(false); //TODO Why?
handler.removeCallbacks(registerLocationRunnable);
unregisterLocationListener();
@@ -400,40 +397,6 @@ public class TrackRecordingService extends Service {
return Long.parseLong(uri.getLastPathSegment());
}
/**
* Starts the service as a foreground service.
*
* @param pendingIntent the notification pending intent
* @param messageId the notification message id
*/
@VisibleForTesting
protected void startForegroundService(PendingIntent pendingIntent, int messageId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel notificationChannel = new NotificationChannel(getString(R.string.app_name), getString(R.string.app_name), NotificationManager.IMPORTANCE_DEFAULT);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (manager != null) {
manager.createNotificationChannel(notificationChannel);
}
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.app_name))
.setContentIntent(pendingIntent)
.setContentText(getString(messageId))
.setContentTitle(getString(R.string.app_name))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_logo_color_24dp)
.setCategory(Notification.CATEGORY_SERVICE)
.setWhen(System.currentTimeMillis());
startForeground(1, builder.build());
}
/**
* Stops the service as a foreground service.
*/
@VisibleForTesting
protected void stopForegroundService() {
stopForeground(true);
}
/**
* Handles start command.
*
@@ -468,12 +431,12 @@ public class TrackRecordingService extends Service {
}
PreferencesUtils.incrementAutoResumeTrackCurrentRetryDefault(this);
if (autoResumeTrackTimeout == Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_never))) {
if (autoResumeTrackTimeout == Integer.parseInt(getResources().getString(R.string.auto_resume_track_timeout_never))) {
Log.d(TAG, "Not resuming. Auto-resume track timeout set to never.");
return false;
}
if (autoResumeTrackTimeout == Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_always))) {
if (autoResumeTrackTimeout == Integer.parseInt(getResources().getString(R.string.auto_resume_track_timeout_always))) {
Log.d(TAG, "Resuming. Auto-resume track timeout set to always.");
return true;
}
@@ -544,7 +507,7 @@ public class TrackRecordingService extends Service {
}
markerTripStatisticsUpdater = new TripStatisticsUpdater(markerStartTime);
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
while (locationIterator.hasNext()) {
Location location = locationIterator.next();
@@ -677,6 +640,8 @@ public class TrackRecordingService extends Service {
}
endRecording(false, recordingTrackId);
notificationManager.updateContent(getString(R.string.generic_paused));
}
/**
@@ -763,6 +728,8 @@ public class TrackRecordingService extends Service {
return;
}
notificationManager.updateLocation(this, location, recordingGpsAccuracy);
if (!location.hasAccuracy() || location.getAccuracy() >= recordingGpsAccuracy) {
Log.d(TAG, "Ignore onLocationChangedAsync. Poor accuracy.");
return;
@@ -798,10 +765,7 @@ public class TrackRecordingService extends Service {
}
if (!LocationUtils.isValidLocation(lastValidTrackPoint)) {
/*
* Should not happen. The current segment should have a location. Just
* insert the current location.
*/
// Should not happen. The current segment should have a location. Just insert the current location.
insertLocation(track, location, null);
lastLocation = location;
return;
@@ -948,33 +912,33 @@ public class TrackRecordingService extends Service {
}
}
/**
* Shows the notification.
*
* @param isGpsStarted true if GPS is started
*/
private void showNotification(boolean isGpsStarted) {
if (isRecording()) {
if (isPaused()) {
stopForegroundService();
} else {
Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId);
PendingIntent pendingIntent = TaskStackBuilder.create(this)
.addParentStack(TrackDetailActivity.class).addNextIntent(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
startForegroundService(pendingIntent, R.string.track_record_notification);
}
} else {
// Not recording
if (isGpsStarted) {
Intent intent = IntentUtils.newIntent(this, TrackListActivity.class);
PendingIntent pendingIntent = TaskStackBuilder.create(this)
.addNextIntent(intent).getPendingIntent(0, 0);
startForegroundService(pendingIntent, R.string.gps_starting);
} else {
stopForegroundService();
}
if ((isRecording() && isPaused()) || (!isRecording() && !isGpsStarted)) {
stopForeground(true);
}
if (isRecording() && !isPaused()) {
Intent intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, recordingTrackId);
PendingIntent pendingIntent = TaskStackBuilder.create(this)
.addParentStack(TrackDetailActivity.class)
.addNextIntent(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
notificationManager.updatePendingIntent(pendingIntent);
notificationManager.updateContent(getString(R.string.gps_starting));
startForeground(NOTIFICATION_ID, notificationManager.getNotification());
}
if (!isRecording() && isGpsStarted) {
Intent intent = IntentUtils.newIntent(this, TrackListActivity.class);
PendingIntent pendingIntent = TaskStackBuilder.create(this)
.addNextIntent(intent)
.getPendingIntent(0, 0);
notificationManager.updatePendingIntent(pendingIntent);
notificationManager.updateContent(getString(R.string.gps_starting));
startForeground(NOTIFICATION_ID, notificationManager.getNotification());
}
}
@@ -0,0 +1,94 @@
package de.dennisguse.opentracks.services;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.location.Location;
import android.os.Build;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Manages the content of the notification shown by {@link TrackRecordingService}.
*/
class TrackRecordingServiceNotificationManager {
private final static int NOTIFICATION_ID = 123;
private final static String CHANNEL_ID = TrackRecordingServiceNotificationManager.class.getSimpleName();
private NotificationCompat.Builder notificationBuilder;
private NotificationManager notificationManager;
private boolean previousLocationWasAccurate = true;
TrackRecordingServiceNotificationManager(Context context) {
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, context.getString(R.string.app_name), NotificationManager.IMPORTANCE_HIGH);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
notificationChannel.setAllowBubbles(true);
}
notificationManager.createNotificationChannel(notificationChannel);
}
notificationBuilder = new NotificationCompat.Builder(context, CHANNEL_ID);
notificationBuilder
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setOnlyAlertOnce(true)
.setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setContentTitle(context.getString(R.string.app_name))
.setSmallIcon(R.drawable.ic_logo_color_24dp);
}
@VisibleForTesting
TrackRecordingServiceNotificationManager(NotificationManager notificationManager, NotificationCompat.Builder notificationBuilder) {
this.notificationManager = notificationManager;
this.notificationBuilder = notificationBuilder;
}
void updateContent(String content) {
notificationBuilder.setContentText(content);
updateNotification();
}
void updateLocation(Context context, Location location, int recordingGpsAccuracy) {
String formattedAccuracy = context.getString(R.string.value_none);
if (location.hasAccuracy()) {
formattedAccuracy = StringUtils.formatDistance(context, location.getAccuracy(), PreferencesUtils.isMetricUnits(context));
boolean currentLocationWasAccurate = location.getAccuracy() < recordingGpsAccuracy;
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
notificationBuilder.setOnlyAlertOnce(!shouldAlert);
previousLocationWasAccurate = currentLocationWasAccurate;
}
notificationBuilder.setContentText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
updateNotification();
notificationBuilder.setOnlyAlertOnce(true);
}
void updatePendingIntent(PendingIntent pendingIntent) {
notificationBuilder.setContentIntent(pendingIntent);
updateNotification();
}
Notification getNotification() {
return notificationBuilder.build();
}
private void updateNotification() {
notificationManager.notify(NOTIFICATION_ID, getNotification());
}
}
@@ -34,6 +34,7 @@ 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.TTSUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
@@ -193,7 +194,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
* @param announcement the announcement
*/
private void speakAnnouncement(String announcement) {
int result = audioManager.requestAudioFocus(null, TextToSpeech.Engine.DEFAULT_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
int result = audioManager.requestAudioFocus(null, TTSUtils.getTTSStream(), AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "Failed to request audio focus.");
}
@@ -147,7 +147,7 @@ public class BluetoothLePreference extends DialogPreference {
BluetoothLePreference preference = (BluetoothLePreference) getPreference();
String deviceSelected = preference.getValue();
if (!deviceNone.equals(deviceSelected)) {
if (deviceSelected != null && !deviceNone.equals(deviceSelected)) {
listAdapter.add(preference.getValue(), preference.getValue());
selectedEntryIndex = 1;
}
@@ -204,12 +204,13 @@ public class FileUtils {
*
* @param name name
*/
// TODO Check if this function is still needed.
public static String sanitizeFileName(String name) {
StringBuilder builder = new StringBuilder(name.length());
for (int i = 0; i < name.length(); i++) {
int codePoint = name.codePointAt(i);
char character = name.charAt(i);
if (Character.isLetterOrDigit(character) || codePoint > 127 || isSpecialFat32(character)) {
if (Character.isLetterOrDigit(character) || codePoint > 127 || isSpecialFat32(character) || character == '.') {
builder.appendCodePoint(codePoint);
} else {
builder.append("_");
@@ -76,7 +76,7 @@ public class IntentUtils {
}
String trackDescription = "";
if (trackIds.length == 1) {
Track track = ContentProviderUtils.Factory.get(context).getTrack(trackIds[0]);
Track track = new ContentProviderUtils(context).getTrack(trackIds[0]);
trackDescription = track == null ? "" : new DescriptionGeneratorImpl(context).generateTrackDescription(track, false);
}
@@ -89,13 +89,13 @@ public class IntentUtils {
uris.add(uriAndMime.first);
mime = uriAndMime.second;
}
return new Intent(action)
.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.share_track_subject))
.putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_track_share_file_body, trackDescription))
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
.setType(mime)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
.setAction(Intent.ACTION_SEND_MULTIPLE)
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.share_track_subject))
.putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_track_share_file_body, trackDescription));
}
public static void showCoordinateOnMap(Context context, Waypoint waypoint) {
@@ -157,7 +157,7 @@ public class LocationUtils {
}
/**
* Checks if a given location is a valid (i.e. physically possible) locationon Earth.
* Checks if a given location is a valid (i.e. physically possible) location on Earth.
* Note: The special separator locations (which have latitude = 100) will not qualify as valid.
* Neither will locations with lat=0 and lng=0 as these are most likely "bad" measurements which often cause trouble.
*
@@ -259,11 +259,6 @@ public class PreferencesUtils {
return PreferencesUtils.getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
}
public static boolean isChartByDistance(Context context) {
final String CHART_X_AXIS_DEFAULT = context.getString(R.string.chart_x_axis_default);
return CHART_X_AXIS_DEFAULT.equals(getString(context, R.string.chart_x_axis_key, CHART_X_AXIS_DEFAULT));
}
public static boolean shouldChartShowCadence(Context context) {
final boolean CHART_SHOW_CADENCE = context.getResources().getBoolean(R.bool.chart_show_cadence_default);
return PreferencesUtils.getBoolean(context, R.string.chart_show_cadence_key, CHART_SHOW_CADENCE);
@@ -0,0 +1,13 @@
package de.dennisguse.opentracks.util;
import android.speech.tts.TextToSpeech;
public class TTSUtils {
private TTSUtils() {
}
public static int getTTSStream() {
return TextToSpeech.Engine.DEFAULT_STREAM;
}
}
+4 -18
View File
@@ -14,22 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/chart_view_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/chart_view_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical" />
<ZoomControls
android:id="@+id/chart_zoom_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:gravity="right"
android:paddingRight="8dp" />
</LinearLayout>
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical" />
+188
View File
@@ -0,0 +1,188 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
style="@style/TextMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/help_feature_title"
android:textStyle="bold" />
<TextView
style="@style/TextMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/help_feature_content" />
<TextView
style="@style/TextMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/help_track_map_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_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"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/help_recording_title"
android:textStyle="bold" />
<TextView
style="@style/TextMedium"
android:layout_width="match_parent"
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"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/help_storage_pictures_title"
android:textStyle="bold" />
<TextView
style="@style/TextMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/help_storage_pictures_content" />
<TextView
style="@style/TextMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/help_track_export_title"
android:textStyle="bold" />
<TextView
style="@style/TextMedium"
android:layout_width="match_parent"
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"
android:layout_height="wrap_content"
android:text="@string/help_data_google_play_services_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"
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"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/help_data_storage_title"
android:textStyle="bold" />
<TextView
style="@style/TextMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/help_data_storage_content" />
</LinearLayout>
</ScrollView>
</LinearLayout>
@@ -15,7 +15,8 @@ limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar" />
-13
View File
@@ -114,19 +114,6 @@
<item>@string/auto_resume_track_timeout_always</item>
</string-array>
<string name="chart_x_axis_key" translatable="false">chartXAxis</string>
<string name="chart_x_axis_default" translatable="false">@string/chart_x_axis_distance</string>
<string-array name="chart_x_axis_values">
<item>@string/chart_x_axis_distance</item>
<item>@string/chart_x_axis_time</item>
</string-array>
<string name="chart_x_axis_time" translatable="false">TIME</string>
<string name="chart_x_axis_distance" translatable="false">DISTANCE</string>
<string-array name="chart_x_axis_options">
<item>@string/settings_chart_by_distance</item>
<item>@string/settings_chart_by_time</item>
</string-array>
<string name="split_frequency_key" translatable="false">splitFrequency</string>
<string name="split_frequency_default" translatable="false">@string/frequency_off</string>
<string name="voice_frequency_key" translatable="false">voiceFrequency</string>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- This file contains old and now unused settings. -->
<!-- It is recommended not to reuse the keys. -->
<string name="chart_x_axis_key" translatable="false">chartXAxis</string>
<string name="chart_x_axis_default" translatable="false">@string/chart_x_axis_distance</string>
<string-array name="chart_x_axis_values">
<item>@string/chart_x_axis_distance</item>
<item>@string/chart_x_axis_time</item>
</string-array>
<string name="chart_x_axis_time" translatable="false">TIME</string>
<string name="chart_x_axis_distance" translatable="false">DISTANCE</string>
<string-array name="chart_x_axis_options">
<item>@string/settings_chart_by_distance</item>
<item>@string/settings_chart_by_time</item>
</string-array>
</resources>
+92
View File
@@ -38,6 +38,95 @@ limitations under the License.
Libraries
</string>
<!-- Help -->
<string name="help_feature_title">
OpenTracks has the following features:
</string>
<string name="help_feature_content">
* Create markers (incl. pictures)\n
* Heart rate sensor (Bluetooth LE)\n
* Voice announcements by time or distance\n
* While recording, show on lock screen\n
* Share tracks
</string>
<string name="help_data_statistics_customized_title">
Can the statistics view be customized?
</string>
<string name="help_data_statistics_customized_content">
Yes, it can be customized in the Settings.
</string>
<string name="help_recording_title">
How does OpenTracks record data?
</string>
<string name="help_recording_content">
OpenTracks uses the GPS sensor of the device to record the location continuously.
</string>
<string name="help_gps_imprecise_title">
Statistics (e.g., speed) reported by OpenTracks are imprecise?
</string>
<string name="help_gps_imprecise_content">
OpenTracks directly uses the data provided by the GPS of the mobile device.
If the GPS device reports inaccurate locations, OpenTracks cannot do anything about it.
</string>
<string name="help_heart_rate_sensors_title">
Which heart rate sensors does OpenTracks support?
</string>
<string name="help_heart_rate_sensors_content">
OpenTracks should work with any Bluetooth LE heart sensor that support streaming the data in real-time.
Smartwatches that store the heart rate data are not supported.
</string>
<string name="help_track_map_title">
How to show a track on a map?
</string>
<string name="help_track_map_content">
This requires a 3rd party application to be installed that can consume KMZ-encoded geo data.
This works well with <a href="http://osmand.net/">OsmAnd~</a> and <a href="https://maps.me/">Maps.ME</a>.
In fact, an App needs to support <i>ACTION_VIEW</i> with MIME <i>application/vnd.google-earth.kmz</i>.
At the moment, the recorded locations as well as the timestamps are send to an App while pictures and sensor data are not shared.
</string>
<string name="help_storage_pictures_title">
How does OpenTracks store pictures that were taken as markers?
</string>
<string name="help_storage_pictures_content">
These are stored directly on the file system.
</string>
<string name="help_track_export_title">
How can I export/share recorded tracks?
</string>
<string name="help_track_export_content">
Tracks can be exported from the Settings as KMZ, KML, or GPX.
KMZ contains all data incl. images, while KML contains only the sensor data, and GPX only the location.
Tracks shared via the <i>share icon</i> are shared as KMZ and any App can be used to send the data.
</string>
<string name="help_data_upload_title">
Can OpenTracks upload the data automatically?
</string>
<string name="help_data_google_play_services_title">
Does OpenTracks require/use Google Play Service?
</string>
<string name="help_data_online_title">
Does OpenTracks use online services?
</string>
<string name="help_data_storage_title">
How does OpenTracks store sensor data?
</string>
<string name="help_data_storage_content">
OpenTracks stores sensor data per recorded location.
In terms of a heart rate sensor this means that only <i>one</i> heart rate measurement is stored per location.
If you are standing still or are indoors, no sensor data will be recorded (but it will be shown).
</string>
<!-- Activity Type -->
<string name="activity_type_airplane" translation_description="When entering an 'Activity type', in the auto-complete text box, the
completion suggestion for airplane.">
@@ -1425,6 +1514,9 @@ limitations under the License.
<string name="track_record_notification" translation_description="When recording a track, the message on the OpenTracks notification
informing the user that OpenTracks is actively recording his track.">
Recording your track&#8230;
</string>
<string name="track_recording_notification_accuracy">
Location accuracy: %1$s
</string>
<string name="track_markers" translation_description="Title indicating a particular track's markers.
+8 -7
View File
@@ -17,6 +17,14 @@ limitations under the License.
xmlns:app="http://schemas.android.com/apk/res-auto"
android:title="@string/menu_settings">
<PreferenceCategory android:title="@string/menu_help">
<Preference android:title="@string/menu_help">
<intent
android:targetClass="de.dennisguse.opentracks.HelpActivity"
android:targetPackage="@string/applicationId" />
</Preference>
</PreferenceCategory>
<PreferenceCategory android:title="@string/track_detail_stats_tab">
<ListPreference
android:defaultValue="@string/stats_units_default"
@@ -46,13 +54,6 @@ limitations under the License.
</PreferenceCategory>
<PreferenceCategory android:title="@string/track_detail_chart_tab">
<ListPreference
android:defaultValue="@string/chart_x_axis_default"
android:entries="@array/chart_x_axis_options"
android:entryValues="@array/chart_x_axis_values"
android:key="@string/chart_x_axis_key"
android:title="@string/settings_chart_x_axis"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference
android:defaultValue="true"
android:key="@string/chart_show_elevation_key"