Compare commits

..

38 Commits

Author SHA1 Message Date
Dennis Guse 9bafc7d4a1 Release v3.2.9 2019-12-04 19:57:27 +01:00
Dennis Guse be58b48d20 Bugfix: StatsFragment (onResume and track was recording) overwrote in the total time UI until next UI update. 2019-12-03 22:21:45 +01:00
Dennis Guse 303abac980 Removed READ_PHONE_STATE permission and use AudioManager.MODE_IN_CALL instead. 2019-12-03 22:01:01 +01:00
Dennis Guse d1a459efc2 Get audio focus onStart. 2019-12-03 20:28:22 +01:00
Dennis Guse 96069e6f09 Cleanup. 2019-12-03 20:24:57 +01:00
Dennis Guse 301a721329 Settings: localize no heart rate sensor selected. 2019-12-03 19:53:14 +01:00
Dennis Guse e8c6801a32 Refactor StatsFragment. 2019-12-03 19:19:19 +01:00
Dennis Guse 355f3ef614 Added "screen always on"-feature to TrackDetailActivity.
And the "show on lockscreen"-feature is only used while recording.
2019-12-03 16:57:56 +01:00
Dennis Guse eaa6083586 Bugfix: Heart rate was not shown in stats. 2019-12-03 07:31:29 +01:00
Dennis Guse a0c5d02c51 Fixed and removed device features (AndroidManifest). 2019-12-03 07:31:29 +01:00
Dennis Guse 76f9f5fc82 MarkerDetailFragment now shows the blue pin as default image. 2019-12-03 07:31:29 +01:00
Dennis Guse 8e2598e93b Removed split markers. 2019-12-03 07:31:24 +01:00
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
87 changed files with 2462 additions and 2343 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,10 @@
v3.2.9: OpenTracks
Changes:
- Option to keep screen on while recording
- Markers: removed split markers completely
- Voice announcement now work without READ_PHONE_STATE permission
Bugfixes:
- Statistics: heart rate was not shown
- Statistics: time was briefly reset to 0s
@@ -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);
@@ -151,27 +151,6 @@ public class TrackRecordingServiceTestLooper {
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_splitFrequencyDefault() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.split_frequency_key, Integer.parseInt(context.getResources().getString(R.string.split_frequency_default)));
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_splitFrequencyByDistance() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.split_frequency_key, -1);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_splitFrequencyByTime() throws TimeoutException {
PreferencesUtils.setInt(context, R.string.split_frequency_key, 1);
fullRecordingSession();
}
@MediumTest
@Test
public void testWithProperties_metricUnitsDefault() throws TimeoutException {
@@ -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.
+12 -26
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="3175"
android:versionName="v3.2.9"> <!-- 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" />
@@ -139,34 +145,17 @@ limitations under the License.
android:name="android.hardware.location.gps"
android:required="true" />
<uses-feature
android:name="android.hardware.bluetooth"
android:name="android.hardware.android.hardware.bluetooth_le"
android:required="false" />
<!-- Permissions to access sensors -->
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.faketouch"
android:required="false" />
<!-- Permission to read phone state so voice announcement will not interrupt ongoing calls -->
<uses-feature
android:name="android.hardware.sensor.compass"
android:required="false" />
<!-- Permission to read/write external storage -->
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-feature
android:name="android.hardware.telephony.gsm"
android:required="false" />
<!-- Required features -->
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<!-- Optional features -->
<uses-feature
android:name="android.hardware.touchscreen.multitouch"
android:required="false" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -174,9 +163,6 @@ limitations under the License.
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>
@@ -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<>();
@@ -71,8 +71,7 @@ public class MarkerEditActivity extends AbstractActivity {
waypointSection = findViewById(R.id.marker_edit_waypoint_section);
waypointName = findViewById(R.id.marker_edit_waypoint_name);
waypointMarkerType = findViewById(R.id.marker_edit_waypoint_marker_type);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
this, R.array.waypoint_types, android.R.layout.simple_dropdown_item_1line);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.waypoint_types, android.R.layout.simple_dropdown_item_1line);
waypointMarkerType.setAdapter(adapter);
waypointDescription = findViewById(R.id.marker_edit_waypoint_description);
@@ -127,7 +126,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 +135,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 +181,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);
}
}
@@ -39,13 +39,13 @@ import androidx.loader.content.Loader;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointsColumns;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
@@ -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);
@@ -152,7 +152,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override
public void bindView(View view, Context context, Cursor cursor) {
int typeIndex = cursor.getColumnIndex(WaypointsColumns.TYPE);
int nameIndex = cursor.getColumnIndex(WaypointsColumns.NAME);
int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
int categoryIndex = cursor.getColumnIndex(WaypointsColumns.CATEGORY);
@@ -161,12 +160,11 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
int latitudeIndex = cursor.getColumnIndex(WaypointsColumns.LATITUDE);
int longitudeIndex = cursor.getColumnIndex(WaypointsColumns.LONGITUDE);
boolean statistics = WaypointType.values()[cursor.getInt(typeIndex)] == WaypointType.STATISTICS;
int iconId = statistics ? R.drawable.ic_marker_yellow_pushpin : R.drawable.ic_marker_blue_pushpin;
int iconId = MarkerUtils.ICON_ID;
String name = cursor.getString(nameIndex);
long time = cursor.getLong(timeIndex);
String category = statistics ? null : cursor.getString(categoryIndex);
String description = statistics ? null : cursor.getString(descriptionIndex);
String category = cursor.getString(categoryIndex);
String description = cursor.getString(descriptionIndex);
String photoUrl = cursor.getString(photoUrlIndex);
double latitude = cursor.getDouble(latitudeIndex);
double longitude = cursor.getDouble(longitudeIndex);
@@ -46,7 +46,6 @@ import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
@@ -55,6 +54,7 @@ import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -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);
@@ -393,11 +393,9 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
}
}
boolean statistics = waypoint.getType() == WaypointType.STATISTICS;
resultMap.put(IS_RECORDING_FIELD, false);
resultMap.put(IS_PAUSED_FIELD, true);
resultMap.put(ICON_ID_FIELD, statistics ? R.drawable.ic_marker_yellow_pushpin : R.drawable.ic_marker_blue_pushpin);
resultMap.put(ICON_ID_FIELD, MarkerUtils.ICON_ID);
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_marker);
resultMap.put(NAME_FIELD, waypoint.getName());
// Display the marker's track name in the total time field
@@ -405,8 +403,8 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
resultMap.put(TOTAL_DISTANCE_FIELD, null);
resultMap.put(MARKER_COUNT_FIELD, 0);
resultMap.put(START_TIME_FIELD, waypoint.getLocation().getTime());
resultMap.put(CATEGORY_FIELD, statistics ? null : waypoint.getCategory());
resultMap.put(DESCRIPTION_FIELD, statistics ? null : waypoint.getDescription());
resultMap.put(CATEGORY_FIELD, waypoint.getCategory());
resultMap.put(DESCRIPTION_FIELD, waypoint.getDescription());
resultMap.put(PHOTO_URL_FIELD, waypoint.getPhotoUrl());
resultMap.put(TRACK_ID_FIELD, waypoint.getTrackId());
resultMap.put(MARKER_ID_FIELD, waypoint.getId());
@@ -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;
@@ -54,7 +55,6 @@ import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StatsUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils;
@@ -126,16 +126,24 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(TrackDetailActivity.this);
setLockscreenPolicy();
setScreenOnPolicy();
}
if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(TrackDetailActivity.this);
setLockscreenPolicy();
setScreenOnPolicy();
}
if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.stats_show_on_lockscreen_while_recording_key, key)) {
setLockscreenPolicy();
}
if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.stats_keep_screen_on_while_recording_key, key)) {
setScreenOnPolicy();
}
if (key == null) return;
runOnUiThread(new Runnable() {
@@ -188,7 +196,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 +213,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));
@@ -223,11 +234,13 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
@Override
public void onAttachedToWindow() {
setLockscreenPolicy();
setScreenOnPolicy();
super.onAttachedToWindow();
}
private void setLockscreenPolicy() {
boolean showOnLockScreen = PreferencesUtils.shouldShowStatsOnLockscreen(TrackDetailActivity.this);
boolean showOnLockScreen = PreferencesUtils.shouldShowStatsOnLockscreen(TrackDetailActivity.this)
&& PreferencesUtils.isRecording(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(showOnLockScreen);
@@ -238,6 +251,17 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
}
}
private void setScreenOnPolicy() {
boolean keepScreenOn = PreferencesUtils.shouldKeepScreenOn(TrackDetailActivity.this)
&& PreferencesUtils.isRecording(this);
if (keepScreenOn) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}
@Override
protected void onStart() {
super.onStart();
@@ -409,22 +433,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
return trackDataHub;
}
/**
* Gets the track id.
*/
// TODO Seems to be no used.
@Deprecated
public long getTrackId() {
return trackId;
}
private void setBluetoothHeartRateSensorName(String sensorName) {
StatsUtils.setBluetoothHeartRateSensorName(this, sensorName, isRecording());
}
/**
* Handles the data in the intent.
*/
private void handleIntent(Intent intent) {
trackId = intent.getLongExtra(EXTRA_TRACK_ID, -1L);
long markerId = intent.getLongExtra(EXTRA_MARKER_ID, -1L);
@@ -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,23 +27,29 @@ 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;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.StringUtils;
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 +97,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 +111,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,
@@ -194,12 +264,9 @@ public class ChartView extends View {
pointer = context.getResources().getDrawable(R.drawable.ic_logo_color_24dp);
pointer.setBounds(0, 0, pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight());
Drawable statisticsMarker = getResources().getDrawable(R.drawable.ic_marker_yellow_pushpin);
markerWidth = statisticsMarker.getIntrinsicWidth();
markerHeight = statisticsMarker.getIntrinsicHeight();
statisticsMarker.setBounds(0, 0, markerWidth, markerHeight);
Drawable waypointMarker = getResources().getDrawable(R.drawable.ic_marker_blue_pushpin);
Drawable waypointMarker = MarkerUtils.getDefaultPhoto(context);
markerWidth = waypointMarker.getIntrinsicWidth();
markerHeight = waypointMarker.getIntrinsicHeight();
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
scroller = new Scroller(context);
@@ -222,16 +289,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 +467,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 +624,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 +724,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 +859,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();
@@ -468,12 +466,8 @@ public class TrackDataHub implements DataSourceListener {
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
}
if (!LocationUtils.isValidLocation(location)) {
// TODO: also include the last valid point before a split
for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSegmentSplit(location);
includeNextPoint = true;
}
if (!LocationUtils.isValidLocation(location)) { //This can be split markers (not anymore supported feature)
includeNextPoint = true;
} else {
// Also include the last point if the selected track is not recording.
if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0) || (locationId == lastTrackPointId && !isSelectedTrackRecording())) {
@@ -51,14 +51,9 @@ public interface TrackDataListener {
*/
void onSampledOutTrackPoint(Location location);
/**
* Called when an invalid track point representing a segment split is read.
*/
void onSegmentSplit(Location location);
/**
* Called when finish sending new track points.
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(Location)}, {@link #onSampledOutTrackPoint(Location)} and {@link #onSegmentSplit(Location)}.
* This gets called after every batch of calls to {@link #onSampledInTrackPoint(Location)} and {@link #onSampledOutTrackPoint(Location)}.
*/
void onNewTrackPointsDone();
@@ -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()) {
@@ -199,13 +198,6 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
}
@Override
public void onSegmentSplit(Location location) {
if (isResumed()) {
fillDataPoint(location, null);
}
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
@@ -304,12 +296,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 +376,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));
@@ -41,12 +41,10 @@ import androidx.fragment.app.FragmentActivity;
import de.dennisguse.opentracks.MarkerEditActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.StatsUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -62,7 +60,7 @@ public class MarkerDetailFragment extends Fragment {
private static final long HIDE_TEXT_DELAY = 4 * UnitConversions.ONE_SECOND;
private ContentProviderUtils contentProviderUtils;
private Handler handler;
private ImageView photo;
private ImageView photoView;
private ImageView textGradient;
private LinearLayout waypointInfo;
private Waypoint waypoint;
@@ -110,7 +108,7 @@ public class MarkerDetailFragment extends Fragment {
getFragmentManager().popBackStack();
return;
}
contentProviderUtils = ContentProviderUtils.Factory.get(getActivity());
contentProviderUtils = new ContentProviderUtils(getActivity());
handler = new Handler();
setHasOptionsMenu(true);
}
@@ -119,11 +117,11 @@ public class MarkerDetailFragment extends Fragment {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.marker_detail_fragment, container, false);
photo = view.findViewById(R.id.marker_detail_waypoint_photo);
photoView = view.findViewById(R.id.marker_detail_waypoint_photo);
textGradient = view.findViewById(R.id.marker_detail_waypoint_text_gradient);
waypointInfo = view.findViewById(R.id.marker_detail_waypoint_info);
photo.setOnClickListener(new View.OnClickListener() {
photoView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handler.removeCallbacks(hideText);
@@ -156,11 +154,8 @@ public class MarkerDetailFragment extends Fragment {
@Override
public void setMenuVisibility(boolean menuVisible) {
super.setMenuVisibility(menuVisible);
/*
* View pager caches the neighboring fragments in the resumed state. If
* becoming visible from the resumed state, update the UI to display the
* text above the image.
*/
// View pager caches the neighboring fragments in the resumed state.
// If becoming visible from the resumed state, update the UI to display the text above the image.
if (isResumed()) {
if (menuVisible) {
updateUi();
@@ -214,47 +209,25 @@ public class MarkerDetailFragment extends Fragment {
}
}
/**
* Updates the UI.
*/
private void updateUi() {
View waypointView = getView().findViewById(R.id.marker_detail_waypoint);
View statisticsView = getView().findViewById(R.id.marker_detail_statistics);
boolean isWaypoint = waypoint.getType() == WaypointType.WAYPOINT;
waypointView.setVisibility(isWaypoint ? View.VISIBLE : View.GONE);
statisticsView.setVisibility(isWaypoint ? View.GONE : View.VISIBLE);
if (isWaypoint) {
boolean hasPhoto = waypoint.hasPhoto();
photo.setVisibility(hasPhoto ? View.VISIBLE : View.GONE);
textGradient.setVisibility(hasPhoto ? View.VISIBLE : View.GONE);
waypointInfo.setVisibility(View.VISIBLE);
if (hasPhoto) {
handler.removeCallbacks(hideText);
photo.setImageURI(waypoint.getPhotoURI());
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
}
setName(R.id.marker_detail_waypoint_name, hasPhoto);
TextView category = getView().findViewById(R.id.marker_detail_waypoint_category);
ListItemUtils.setTextView(getActivity(), category, StringUtils.getCategory(waypoint.getCategory()), hasPhoto);
TextView description = getView().findViewById(R.id.marker_detail_waypoint_description);
ListItemUtils.setTextView(getActivity(), description, waypoint.getDescription(), hasPhoto);
setLocation(R.id.marker_detail_waypoint_location, hasPhoto);
boolean hasPhoto = waypoint.hasPhoto();
if (hasPhoto) {
handler.removeCallbacks(hideText);
photoView.setImageURI(waypoint.getPhotoURI());
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
} else {
setName(R.id.marker_detail_statistics_name, false);
setLocation(R.id.marker_detail_statistics_location, false);
Track track = contentProviderUtils.getTrack(waypoint.getTrackId());
StatsUtils.setTripStatisticsValues(getActivity(), waypoint.getTripStatistics(), null);
StatsUtils.setLocationValues(getActivity(), waypoint.getLocation(), false);
photoView.setImageResource(MarkerUtils.ICON_ID);
}
setName(R.id.marker_detail_waypoint_name, hasPhoto);
TextView category = getView().findViewById(R.id.marker_detail_waypoint_category);
ListItemUtils.setTextView(getActivity(), category, StringUtils.getCategory(waypoint.getCategory()), hasPhoto);
TextView description = getView().findViewById(R.id.marker_detail_waypoint_description);
ListItemUtils.setTextView(getActivity(), description, waypoint.getDescription(), hasPhoto);
setLocation(R.id.marker_detail_waypoint_location, hasPhoto);
}
private void setName(int resId, boolean addShadow) {
@@ -20,16 +20,18 @@ import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import java.util.EnumSet;
@@ -43,9 +45,11 @@ import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.StatsUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -59,6 +63,8 @@ public class StatsFragment extends Fragment implements TrackDataListener {
public static final String STATS_FRAGMENT_TAG = StatsFragment.class.getSimpleName();
private static final long UI_UPDATE_INTERVAL = UnitConversions.ONE_SECOND;
private TrackDataHub trackDataHub;
private Handler handlerUpdateUI;
@@ -67,18 +73,6 @@ public class StatsFragment extends Fragment implements TrackDataListener {
private Location lastLocation = null;
private TripStatistics lastTripStatistics = null;
private final Runnable updateUIeachSecond = new Runnable() {
public void run() {
if (isResumed() && isSelectedTrackRecording()) {
if (!isSelectedTrackPaused() && lastTripStatistics != null) {
StatsUtils.setTotalTimeValue(getActivity(), System.currentTimeMillis() - lastTripStatistics.getStopTime() + lastTripStatistics.getTotalTime());
updateSensorDataUI();
}
handlerUpdateUI.postDelayed(this, UnitConversions.ONE_SECOND);
}
}
};
private String category = "";
private int recordingGpsAccuracy;
@@ -87,6 +81,111 @@ public class StatsFragment extends Fragment implements TrackDataListener {
return inflater.inflate(R.layout.stats, container, false);
}
/* Views */
private View sensorContainerView;
private TextView heartRateValueView;
private TextView heartRateSensorView;
private TextView totalTimeValueView;
private final Runnable updateUIeachSecond = new Runnable() {
public void run() {
if (isResumed() && isSelectedTrackRecording()) {
if (!isSelectedTrackPaused() && lastTripStatistics != null) {
updateTotalTime();
updateSensorDataUI();
}
handlerUpdateUI.postDelayed(this, UI_UPDATE_INTERVAL);
}
}
};
private TextView distanceValue;
private TextView distanceUnit;
private View activityLabel;
private Spinner activitySpinner;
private TextView movingTimeValue;
private TextView speedAvgLabel;
private TextView speedAvgValue;
private TextView speedAvgUnit;
private TextView speedMaxLabel;
private TextView speedMaxValue;
private TextView speedMaxUnit;
private TextView speedMovingLabel;
private TextView speedMovingValue;
private TextView speedMovingUnit;
private View gradeElevationSeparator;
private View gradeElevationContainer;
private TextView elevationMinValue;
private TextView elevationMinUnit;
private TextView elevationMaxValue;
private TextView elevationMaxUnit;
private View speedContainer;
private TextView speedLabel;
private TextView speedValue;
private TextView speedUnit;
private View elevationContainer;
private TextView elevationValue;
private TextView elevationUnit;
private View coordinateSeparator;
private View coordinateContainer;
private TextView latitudeValue;
private TextView longitudeValue;
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
sensorContainerView = view.findViewById(R.id.stats_sensor_container);
heartRateValueView = view.findViewById(R.id.stats_sensor_heart_rate_value);
heartRateSensorView = view.findViewById(R.id.stats_sensor_heart_rate_sensor_value);
totalTimeValueView = view.findViewById(R.id.stats_total_time_value);
distanceValue = view.findViewById(R.id.stats_distance_value);
distanceUnit = view.findViewById(R.id.stats_distance_unit);
activityLabel = view.findViewById(R.id.stats_activity_type_label);
activitySpinner = view.findViewById(R.id.stats_activity_type_icon);
movingTimeValue = view.findViewById(R.id.stats_moving_time_value);
speedAvgLabel = view.findViewById(R.id.stats_average_speed_label);
speedAvgValue = view.findViewById(R.id.stats_average_speed_value);
speedAvgUnit = view.findViewById(R.id.stats_average_speed_unit);
speedMaxLabel = view.findViewById(R.id.stats_max_speed_label);
speedMaxValue = view.findViewById(R.id.stats_max_speed_value);
speedMaxUnit = view.findViewById(R.id.stats_max_speed_unit);
speedMovingLabel = view.findViewById(R.id.stats_moving_speed_label);
speedMovingValue = view.findViewById(R.id.stats_moving_speed_value);
speedMovingUnit = view.findViewById(R.id.stats_moving_speed_unit);
gradeElevationSeparator = view.findViewById(R.id.stats_elevation_separator);
gradeElevationContainer = view.findViewById(R.id.stats_elevation_container);
elevationMinValue = view.findViewById(R.id.stats_elevation_min_value);
elevationMinUnit = view.findViewById(R.id.stats_elevation_min_unit);
elevationMaxValue = view.findViewById(R.id.stats_elevation_max_value);
elevationMaxUnit = view.findViewById(R.id.stats_elevation_max_unit);
speedContainer = view.findViewById(R.id.stats_speed);
speedLabel = view.findViewById(R.id.stats_speed_label);
speedValue = view.findViewById(R.id.stats_speed_value);
speedUnit = view.findViewById(R.id.stats_speed_unit);
elevationContainer = view.findViewById(R.id.stats_elevation);
elevationValue = view.findViewById(R.id.stats_elevation_current_value);
elevationUnit = view.findViewById(R.id.stats_elevation_current_unit);
coordinateSeparator = view.findViewById(R.id.stats_coordinate_separator);
coordinateContainer = view.findViewById(R.id.stats_coordinate_container);
latitudeValue = view.findViewById(R.id.stats_latitude_value);
longitudeValue = view.findViewById(R.id.stats_longitude_value);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
@@ -125,10 +224,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null);
trackRecordingServiceConnection.startConnection(getContext());
updateUi(getActivity());
if (isSelectedTrackRecording()) {
handlerUpdateUI.post(updateUIeachSecond);
}
handlerUpdateUI.post(updateUIeachSecond);
}
@Override
@@ -145,6 +241,60 @@ public class StatsFragment extends Fragment implements TrackDataListener {
trackRecordingServiceConnection = null;
}
@Override
public void onDestroyView() {
super.onDestroyView();
sensorContainerView = null;
heartRateValueView = null;
heartRateSensorView = null;
totalTimeValueView = null;
distanceValue = null;
distanceUnit = null;
activityLabel = null;
activitySpinner = null;
movingTimeValue = null;
speedAvgLabel = null;
speedAvgValue = null;
speedAvgUnit = null;
speedMaxLabel = null;
speedMaxValue = null;
speedMaxUnit = null;
speedMovingLabel = null;
speedMovingValue = null;
speedMovingUnit = null;
gradeElevationSeparator = null;
gradeElevationContainer = null;
elevationMinValue = null;
elevationMinUnit = null;
elevationMaxValue = null;
elevationMaxUnit = null;
speedContainer = null;
speedLabel = null;
speedValue = null;
speedUnit = null;
elevationContainer = null;
elevationValue = null;
elevationUnit = null;
coordinateSeparator = null;
coordinateContainer = null;
latitudeValue = null;
longitudeValue = null;
}
@Override
public void onTrackUpdated(final Track track) {
if (isResumed()) {
@@ -154,7 +304,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
if (isResumed()) {
lastTripStatistics = track != null ? track.getTripStatistics() : null;
category = track != null ? track.getCategory() : "";
updateUi(getActivity());
updateUI();
}
}
});
@@ -176,11 +326,6 @@ public class StatsFragment extends Fragment implements TrackDataListener {
lastLocation = location;
}
@Override
public void onSegmentSplit(Location location) {
// We don't care.
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
@@ -200,7 +345,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
lastLocation = null;
}
}
StatsUtils.setLocationValues(getActivity(), lastLocation, isSelectedTrackRecording());
setLocationValues();
}
}
});
@@ -229,7 +374,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void run() {
if (isResumed()) {
updateUi(getActivity());
updateUI();
}
}
});
@@ -244,7 +389,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void run() {
if (isResumed()) {
updateUi(getActivity());
updateUI();
}
}
});
@@ -300,6 +445,12 @@ public class StatsFragment extends Fragment implements TrackDataListener {
return trackDataHub != null && trackDataHub.isSelectedTrackPaused();
}
private void updateUI() {
updateStats();
setLocationValues();
updateSensorDataUI();
}
/**
* Tries to fetch most recent {@link SensorDataSet} {@link de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager}.
*/
@@ -314,16 +465,178 @@ public class StatsFragment extends Fragment implements TrackDataListener {
sensorDataSet = trackRecordingService.getSensorData();
}
StatsUtils.setSensorData(getActivity(), sensorDataSet, isSelectedTrackRecording());
setHeartRateSensorData(sensorDataSet, isSelectedTrackRecording());
}
/**
* Updates the UI.
*/
private void updateUi(FragmentActivity activity) {
String trackIconValue = TrackIconUtils.getIconValue(activity, category);
StatsUtils.setTripStatisticsValues(activity, lastTripStatistics, trackIconValue);
StatsUtils.setLocationValues(activity, lastLocation, isSelectedTrackRecording());
updateSensorDataUI();
private void setHeartRateSensorData(SensorDataSet sensorDataSet, boolean isRecording) {
// heart rate
int isVisible = View.VISIBLE;
if (!isRecording || PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(getContext())) {
isVisible = View.INVISIBLE;
}
sensorContainerView.setVisibility(isVisible);
if (isRecording) {
String heartRate = getContext().getString(R.string.value_unknown);
String sensorName = getContext().getString(R.string.value_unknown);
if (sensorDataSet != null && sensorDataSet.isRecent(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS)) {
sensorName = sensorDataSet.getSensorName();
if (sensorDataSet.hasHeartRate()) {
heartRate = StringUtils.formatDecimal(sensorDataSet.getHeartRate(), 0);
}
}
heartRateSensorView.setText(sensorName);
heartRateValueView.setText(heartRate);
}
}
private void updateStats() {
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext());
boolean isRecording = isSelectedTrackRecording();
// Set total distance
{
double totalDistance = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getTotalDistance();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
distanceValue.setText(parts.first);
distanceUnit.setText(parts.second);
}
// Set activity type
{
activityLabel.setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE);
activitySpinner.setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE);
activitySpinner.setEnabled(isRecording);
if (trackIconValue != null) {
TrackIconUtils.setIconSpinner(activitySpinner, trackIconValue);
}
}
// Set time
if (lastTripStatistics != null) {
movingTimeValue.setText(StringUtils.formatElapsedTime(lastTripStatistics.getMovingTime()));
updateTotalTime();
}
// Set average speed/pace
{
double speed = lastTripStatistics != null ? lastTripStatistics.getAverageSpeed() : Double.NaN;
speedAvgLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
speedAvgValue.setText(parts.first);
speedAvgUnit.setText(parts.second);
}
// Set max speed/pace
{
double speed = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getMaxSpeed();
speedMaxLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
speedMaxValue.setText(parts.first);
speedMaxUnit.setText(parts.second);
}
// Set moving speed/pace
{
double speed = lastTripStatistics != null ? lastTripStatistics.getAverageMovingSpeed() : Double.NaN;
speedMovingLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
speedMovingValue.setText(parts.first);
speedMovingUnit.setText(parts.second);
}
// Set elevation
{
boolean showElevation = PreferencesUtils.isShowStatsGradeElevation(getContext());
gradeElevationSeparator.setVisibility(showElevation ? View.VISIBLE : View.GONE);
gradeElevationContainer.setVisibility(showElevation ? View.VISIBLE : View.GONE);
if (showElevation) {
{
double elevation = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getMinElevation();
Pair<String, String> parts = StringUtils.formatElevation(getContext(), elevation, metricUnits);
elevationMinValue.setText(parts.first);
elevationMinUnit.setText(parts.second);
}
{
double elevation = lastTripStatistics == null ? Double.NaN : lastTripStatistics.getMaxElevation();
Pair<String, String> parts = StringUtils.formatElevation(getContext(), elevation, metricUnits);
elevationMaxValue.setText(parts.first);
elevationMaxUnit.setText(parts.second);
}
}
}
}
private void updateTotalTime() {
long totalTime;
if (isSelectedTrackRecording()) {
totalTime = System.currentTimeMillis() - lastTripStatistics.getStopTime() + lastTripStatistics.getTotalTime();
} else {
totalTime = lastTripStatistics.getTotalTime();
}
totalTimeValueView.setText(StringUtils.formatElapsedTime(totalTime));
}
private void setLocationValues() {
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext());
boolean isRecording = isSelectedTrackRecording();
// Set speed/pace
speedContainer.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
speedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
double speed = lastLocation != null && lastLocation.hasSpeed() ? lastLocation.getSpeed() : Double.NaN;
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
speedValue.setText(parts.first);
speedUnit.setText(parts.second);
}
// Set elevation
boolean showGradeElevation = isRecording && PreferencesUtils.isShowStatsGradeElevation(getContext());
elevationContainer.setVisibility(showGradeElevation ? View.VISIBLE : View.GONE);
if (showGradeElevation) {
double altitude = lastLocation != null && lastLocation.hasAltitude() ? lastLocation.getAltitude() : Double.NaN;
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, metricUnits);
elevationValue.setText(parts.first);
elevationUnit.setText(parts.second);
}
// Set coordinate
boolean showCoordinate = isRecording && PreferencesUtils.isStatsShowCoordinate(getContext());
coordinateSeparator.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
coordinateContainer.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
if (showCoordinate) {
double latitude = lastLocation != null ? lastLocation.getLatitude() : Double.NaN;
String latitudeText = Double.isNaN(latitude) || Double.isInfinite(latitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(latitude);
latitudeValue.setText(latitudeText);
double longitude = lastLocation != null ? lastLocation.getLongitude() : Double.NaN;
String longitudeText = Double.isNaN(longitude) || Double.isInfinite(longitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(longitude);
longitudeValue.setText(longitudeText);
}
}
}
@@ -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;
@@ -60,7 +56,6 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
import de.dennisguse.opentracks.services.tasks.SplitPeriodicTaskFactory;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.stats.TripStatisticsUpdater;
import de.dennisguse.opentracks.util.IntentUtils;
@@ -80,13 +75,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 +90,12 @@ 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 +111,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)) {
@@ -129,14 +124,10 @@ public class TrackRecordingService extends Service {
if (PreferencesUtils.isKey(context, R.string.stats_units_key, key)) {
boolean metricUnits = PreferencesUtils.isMetricUnits(context);
voiceExecutor.setMetricUnits(metricUnits);
splitExecutor.setMetricUnits(metricUnits);
}
if (PreferencesUtils.isKey(context, R.string.voice_frequency_key, key)) {
voiceExecutor.setTaskFrequency(PreferencesUtils.getVoiceFrequency(context));
}
if (PreferencesUtils.isKey(context, R.string.split_frequency_key, key)) {
splitExecutor.setTaskFrequency(PreferencesUtils.getSplitFrequency(context));
}
if (PreferencesUtils.isKey(context, R.string.min_recording_interval_key, key)) {
int minRecordingInterval = PreferencesUtils.getMinRecordingInterval(context);
if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptBatteryLife(context)) {
@@ -218,14 +209,13 @@ 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 +257,7 @@ public class TrackRecordingService extends Service {
}
// Reverse order from onCreate
showNotification(false);
showNotification(false); //TODO Why?
handler.removeCallbacks(registerLocationRunnable);
unregisterLocationListener();
@@ -275,12 +265,6 @@ public class TrackRecordingService extends Service {
// unregister sharedPreferences before shutting down splitExecutor and voiceExecutor
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
try {
splitExecutor.shutdown();
} finally {
splitExecutor = null;
}
try {
voiceExecutor.shutdown();
} finally {
@@ -400,40 +384,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 +418,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 +494,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();
@@ -603,7 +553,6 @@ public class TrackRecordingService extends Service {
// Restore periodic tasks
voiceExecutor.restore();
splitExecutor.restore();
}
/**
@@ -677,6 +626,8 @@ public class TrackRecordingService extends Service {
}
endRecording(false, recordingTrackId);
notificationManager.updateContent(getString(R.string.generic_paused));
}
/**
@@ -688,7 +639,6 @@ public class TrackRecordingService extends Service {
private void endRecording(boolean trackStopped, long trackId) {
// Shutdown periodic tasks
voiceExecutor.shutdown();
splitExecutor.shutdown();
// Update instance variables
if (remoteSensorManager != null) {
@@ -763,6 +713,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 +750,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;
@@ -876,7 +825,6 @@ public class TrackRecordingService extends Service {
Log.w(TAG, "SQLiteException", e);
}
voiceExecutor.update();
splitExecutor.update();
}
/**
@@ -948,33 +896,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());
}
}
@@ -86,11 +86,11 @@ public class BluetoothRemoteSensorManager {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (bluetoothConnectionManager != null && PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_heart_rate_key, key)) {
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
if (address.equals(PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT)) {
if (PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(context)) {
stop();
return;
}
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
if (bluetoothConnectionManager.isSameBluetoothDevice(address)) {
return;
}
@@ -145,12 +145,11 @@ public class BluetoothRemoteSensorManager {
return;
}
// String address = PreferencesUtils.getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
if (PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(address)) {
if (PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(context)) {
Log.w(TAG, "No bluetooth address.");
return;
}
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
Log.i(TAG, "Connecting to bluetooth address: " + address);
BluetoothDevice device;
@@ -21,12 +21,8 @@ import android.media.AudioManager;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.speech.tts.UtteranceProgressListener;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.util.Locale;
import de.dennisguse.opentracks.R;
@@ -46,17 +42,49 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
/**
* The rate at which announcements are spoken.
*/
@VisibleForTesting
static final float TTS_SPEECH_RATE = 0.9f;
private static final float TTS_SPEECH_RATE = 0.9f;
private static final String TAG = AnnouncementPeriodicTask.class.getSimpleName();
private final Context context;
private final AudioManager audioManager;
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
Log.d(TAG, "Audio focus changed to " + focusChange);
boolean stop = false;
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
stop = false;
break;
case AudioManager.AUDIOFOCUS_LOSS:
stop = true;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
stop = true;
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
stop = true;
break;
}
if (stop && tts != null && tts.isSpeaking()) {
tts.stop();
Log.i(TAG, "Aborting current tts due to focus change " + focusChange);
}
}
};
private final UtteranceProgressListener utteranceListener = new UtteranceProgressListener() {
@Override
public void onStart(String utteranceId) {
int result = audioManager.requestAudioFocus(audioFocusChangeListener, TextToSpeech.Engine.DEFAULT_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "Failed to request audio focus.");
}
}
@Override
@@ -69,32 +97,16 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
@Override
public void onError(String utteranceId) {
Log.e(TAG, "An error occurred for utteranceId " + utteranceId);
}
};
private TextToSpeech tts;
// Response from TTS after its initialization
private int initStatus = TextToSpeech.ERROR;
private int ttsInitStatus = TextToSpeech.ERROR;
private boolean ttsReady = false;
// True if speech is allowed
private boolean speechAllowed;
/**
* Listener which updates {@link #speechAllowed} when the phone state changes.
*/
private final PhoneStateListener phoneStateListener = new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
speechAllowed = state == TelephonyManager.CALL_STATE_IDLE;
if (!speechAllowed && tts != null && tts.isSpeaking()) {
// If we're already speaking, stop it.
tts.stop();
}
}
};
AnnouncementPeriodicTask(Context context) {
this.context = context;
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@@ -102,16 +114,17 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
@Override
public void start() {
Log.d(TAG, "Start");
if (tts == null) {
tts = newTextToSpeech(context, new OnInitListener() {
tts = new TextToSpeech(context, new OnInitListener() {
@Override
public void onInit(int status) {
initStatus = status;
Log.i(TAG, "TextToSpeech initialized with status " + status);
ttsInitStatus = status;
}
});
}
speechAllowed = true;
listenToPhoneState(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
}
@Override
@@ -128,8 +141,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
*
* @param tripStatistics the trip statistics
*/
@VisibleForTesting
void announce(TripStatistics tripStatistics) {
private void announce(TripStatistics tripStatistics) {
if (tripStatistics == null) {
Log.e(TAG, "TripStatistics is null.");
return;
@@ -137,7 +149,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
synchronized (this) {
if (!ttsReady) {
ttsReady = initStatus == TextToSpeech.SUCCESS;
ttsReady = ttsInitStatus == TextToSpeech.SUCCESS;
if (ttsReady) {
onTtsReady();
}
@@ -148,7 +160,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
}
}
if (!speechAllowed) {
if (audioManager.getMode() == AudioManager.MODE_IN_CALL || audioManager.getMode() == AudioManager.MODE_IN_COMMUNICATION) {
Log.i(TAG, "Speech is not allowed at this time.");
return;
}
@@ -157,21 +169,18 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
@Override
public void shutdown() {
listenToPhoneState(phoneStateListener, PhoneStateListener.LISTEN_NONE);
if (tts != null) {
tts.shutdown();
tts = null;
}
}
/**
* Called when TTS is ready.
*/
private void onTtsReady() {
Locale locale = Locale.getDefault();
int languageAvailability = tts.isLanguageAvailable(locale);
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA
|| languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA || languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
Log.w(TAG, "Default locale not available, use English.");
locale = Locale.ENGLISH;
/*
@@ -187,42 +196,12 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
tts.setOnUtteranceProgressListener(utteranceListener);
}
/**
* Speaks the announcement.
*
* @param announcement the announcement
*/
private void speakAnnouncement(String announcement) {
int result = audioManager.requestAudioFocus(null, TextToSpeech.Engine.DEFAULT_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "Failed to request audio focus.");
}
/*
* We don't care about the utterance id. It is supplied here to force
* onUtteranceCompleted to be called.
*/
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
}
/**
* Create a new {@link TextToSpeech}.
*
* @param aContext a context
* @param onInitListener an on init listener
*/
@VisibleForTesting
protected TextToSpeech newTextToSpeech(Context aContext, OnInitListener onInitListener) {
return new TextToSpeech(aContext, onInitListener);
}
/**
* Gets the announcement.
*
* @param tripStatistics the trip statistics
*/
@VisibleForTesting
protected String getAnnouncement(TripStatistics tripStatistics) {
private String getAnnouncement(TripStatistics tripStatistics) {
boolean metricUnits = PreferencesUtils.isMetricUnits(context);
boolean reportSpeed = PreferencesUtils.isReportSpeed(context);
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
@@ -255,27 +234,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
getAnnounceTime(tripStatistics.getMovingTime()), rate);
}
/**
* Listens to phone state.
*
* @param listener the listener
* @param events the interested events
*/
@VisibleForTesting
protected void listenToPhoneState(PhoneStateListener listener, int events) {
TelephonyManager telephony = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (telephony != null) {
telephony.listen(listener, events);
}
}
/**
* Gets the announce time.
*
* @param time the time
*/
@VisibleForTesting
String getAnnounceTime(long time) {
private String getAnnounceTime(long time) {
int[] parts = StringUtils.getTimeParts(time);
String seconds = context.getResources()
.getQuantityString(R.plurals.voiceSeconds, parts[0], parts[0]);
@@ -1,41 +0,0 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.services.tasks;
import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.services.TrackRecordingService;
/**
* A simple task to insert statistics markers periodically.
*
* @author Sandor Dornbush
*/
public class SplitPeriodicTask implements PeriodicTask {
@Override
public void start() {
}
@Override
public void run(TrackRecordingService trackRecordingService) {
trackRecordingService.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
}
@Override
public void shutdown() {
}
}
@@ -1,32 +0,0 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.services.tasks;
import android.content.Context;
/**
* A {@link PeriodicTaskFactory} for {@link SplitPeriodicTask}.
*
* @author Jimmy Shih
*/
public class SplitPeriodicTaskFactory implements PeriodicTaskFactory {
@Override
public PeriodicTask create(Context context) {
return new SplitPeriodicTask();
}
}
@@ -24,6 +24,7 @@ import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.BluetoothUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* Preference to select a discoverable Bluetooth LE device.
@@ -33,6 +34,8 @@ public class BluetoothLePreference extends DialogPreference {
private final static String TAG = BluetoothLePreference.class.getSimpleName();
private final static int DEVICE_NONE_RESOURCEID = R.string.value_none;
public BluetoothLePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@@ -75,6 +78,10 @@ public class BluetoothLePreference extends DialogPreference {
@Override
public CharSequence getSummary() {
if (getValue() == null || PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(getContext(), getValue())) {
return getContext().getString(DEVICE_NONE_RESOURCEID);
}
return getValue();
}
@@ -142,12 +149,12 @@ public class BluetoothLePreference extends DialogPreference {
}
String deviceNone = getContext().getString(R.string.sensor_type_value_none);
listAdapter.add(getContext().getString(R.string.value_none), deviceNone);
listAdapter.add(getContext().getString(DEVICE_NONE_RESOURCEID), deviceNone);
selectedEntryIndex = 0;
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;
}
@@ -167,9 +167,6 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
ListPreference voiceFrequency = findPreference(getString(R.string.voice_frequency_key));
voiceFrequency.setEntries(StringUtils.getFrequencyOptions(getActivity(), metricUnits));
ListPreference splitFrequency = findPreference(getString(R.string.split_frequency_key));
splitFrequency.setEntries(StringUtils.getFrequencyOptions(getActivity(), metricUnits));
ListPreference minRecordingInterval = findPreference(getString(R.string.min_recording_interval_key));
minRecordingInterval.setEntries(PreferenceHelper.getMinRecordingIntervalEntries(getActivity()));
@@ -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.
*
@@ -0,0 +1,20 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.R;
public class MarkerUtils {
public static final int ICON_ID = R.drawable.ic_marker_blue_pushpin;
private MarkerUtils() {
}
public static Drawable getDefaultPhoto(@NonNull Context context) {
return context.getResources().getDrawable(ICON_ID);
}
}
@@ -32,8 +32,6 @@ import de.dennisguse.opentracks.R;
*/
public class PreferencesUtils {
public static final String BLUETOOTH_SENSOR_DEFAULT = "";
private PreferencesUtils() {
}
@@ -251,17 +249,20 @@ public class PreferencesUtils {
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, RECORDING_TRACK_PAUSED);
}
public static String getBluetoothHeartRateSensorAddressNone(Context context) {
return context.getString(R.string.sensor_type_value_none);
}
public static boolean isBluetoothHeartRateSensorAddressDefault(Context context) {
return PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(getBluetoothHeartRateSensorAddress(context));
return getBluetoothHeartRateSensorAddressNone(context).equals(getBluetoothHeartRateSensorAddress(context));
}
public static boolean isBluetoothHeartRateSensorAddressDefault(Context context, String currentValue) {
return getBluetoothHeartRateSensorAddressNone(context).equals(currentValue);
}
public static String getBluetoothHeartRateSensorAddress(Context context) {
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));
return PreferencesUtils.getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothHeartRateSensorAddressNone(context));
}
public static boolean shouldChartShowCadence(Context context) {
@@ -294,6 +295,11 @@ public class PreferencesUtils {
return getBoolean(context, R.string.stats_show_on_lockscreen_while_recording_key, STATS_SHOW_ON_LOCKSCREEN_DEFAULT);
}
public static boolean shouldKeepScreenOn(Context context) {
final boolean DEFAULT = context.getResources().getBoolean(R.bool.stats_keep_screen_on_while_recording_default);
return getBoolean(context, R.string.stats_keep_screen_on_while_recording_key, DEFAULT);
}
public static boolean isShowStatsGradeElevation(Context context) {
final boolean STATS_SHOW_GRADE_ELEVATION = context.getResources().getBoolean(R.bool.stats_show_grade_elevation_default);
return PreferencesUtils.getBoolean(context, R.string.stats_show_grade_elevation_key, STATS_SHOW_GRADE_ELEVATION);
@@ -309,11 +315,6 @@ public class PreferencesUtils {
return PreferencesUtils.getInt(context, R.string.voice_frequency_key, VOICE_FREQUENCY_DEFAULT);
}
public static int getSplitFrequency(Context context) {
final int SPLIT_FREQUENCY_DEFAULT = Integer.parseInt(context.getResources().getString(R.string.split_frequency_default));
return PreferencesUtils.getInt(context, R.string.split_frequency_key, SPLIT_FREQUENCY_DEFAULT);
}
public static int getRecordingDistanceInterval(Context context) {
final int RECORDING_DISTANCE_INTERVAL = Integer.parseInt(context.getResources().getString(R.string.recording_distance_interval_default));
return PreferencesUtils.getInt(context, R.string.recording_distance_interval_key, getRecordingDistanceIntervalDefault(context));
@@ -1,283 +0,0 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.util;
import android.app.Activity;
import android.location.Location;
import android.util.Pair;
import android.view.View;
import android.widget.Spinner;
import android.widget.TextView;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.stats.TripStatistics;
/**
* Utilities for updating the statistics UI labels and values.
*
* @author Jimmy Shih
*/
public class StatsUtils {
private static final String GRADE_PERCENTAGE = "%";
private static final String GRADE_FORMAT = "%1$d";
private StatsUtils() {
}
/**
* Sets the location values.
*
* @param activity the activity for finding views
* @param location the location
* @param isRecording true if recording
*/
public static void setLocationValues(Activity activity, Location location, boolean isRecording) {
boolean metricUnits = PreferencesUtils.isMetricUnits(activity);
boolean reportSpeed = PreferencesUtils.isReportSpeed(activity);
// Set speed/pace
View speedContainer = activity.findViewById(R.id.stats_speed);
speedContainer.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
TextView speedLabel = activity.findViewById(R.id.stats_speed_label);
speedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
double speed = location != null && location.hasSpeed() ? location.getSpeed() : Double.NaN;
Pair<String, String> parts = StringUtils.getSpeedParts(activity, speed, metricUnits, reportSpeed);
TextView speedValue = activity.findViewById(R.id.stats_speed_value);
speedValue.setText(parts.first);
TextView speedUnit = activity.findViewById(R.id.stats_speed_unit);
speedUnit.setText(parts.second);
}
// Set elevation
boolean showGradeElevation = isRecording && PreferencesUtils.isShowStatsGradeElevation(activity);
View elevationContainer = activity.findViewById(R.id.stats_elevation);
elevationContainer.setVisibility(showGradeElevation ? View.VISIBLE : View.GONE);
if (showGradeElevation) {
double altitude = location != null && location.hasAltitude() ? location.getAltitude() : Double.NaN;
Pair<String, String> parts = StringUtils.formatElevation(activity, altitude, metricUnits);
TextView elevationValue = activity.findViewById(R.id.stats_elevation_current_value);
elevationValue.setText(parts.first);
TextView elevationUnit = activity.findViewById(R.id.stats_elevation_current_unit);
elevationUnit.setText(parts.second);
}
// Set coordinate
boolean showCoordinate = isRecording && PreferencesUtils.isStatsShowCoordinate(activity);
View coordinateSeparator = activity.findViewById(R.id.stats_coordinate_separator);
coordinateSeparator.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
View coordinateContainer = activity.findViewById(R.id.stats_coordinate_container);
coordinateContainer.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
if (showCoordinate) {
double latitude = location != null ? location.getLatitude() : Double.NaN;
String latitudeText = Double.isNaN(latitude) || Double.isInfinite(latitude) ? activity.getString(R.string.value_unknown) : StringUtils.formatCoordinate(latitude);
TextView latitudeValue = activity.findViewById(R.id.stats_latitude_value);
latitudeValue.setText(latitudeText);
double longitude = location != null ? location.getLongitude() : Double.NaN;
String longitudeText = Double.isNaN(longitude) || Double.isInfinite(longitude) ? activity.getString(R.string.value_unknown) : StringUtils.formatCoordinate(longitude);
TextView longitudeValue = activity.findViewById(R.id.stats_longitude_value);
longitudeValue.setText(longitudeText);
}
}
public static void setSensorData(Activity activity, SensorDataSet sensorDataSet, boolean isRecording) {
// heart rate
int isVisible = View.VISIBLE;
if (!isRecording || PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(activity)) {
isVisible = View.INVISIBLE;
}
activity.findViewById(R.id.stats_sensor_container).setVisibility(isVisible);
if (isRecording) {
TextView heartRateValue = activity.findViewById(R.id.stats_sensor_heart_rate_value);
TextView heartRateSensor = activity.findViewById(R.id.stats_sensor_heart_rate_sensor_value);
String heartRate = activity.getString(R.string.value_unknown);
String sensor = activity.getString(R.string.value_unknown);
if (sensorDataSet != null && sensorDataSet.isRecent(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS)) {
sensor = sensorDataSet.getSensorName();
if (sensorDataSet.hasHeartRate()) {
heartRate = StringUtils.formatDecimal(sensorDataSet.getHeartRate(), 0);
}
}
setBluetoothHeartRateSensorName(activity, sensor, isRecording);
heartRateSensor.setText(sensor);
}
}
public static void setBluetoothHeartRateSensorName(Activity activity, String sensorName, boolean isRecording) {
if (isRecording) {
TextView heartRateSensor = activity.findViewById(R.id.stats_sensor_heart_rate_sensor_value);
heartRateSensor.setText(sensorName);
}
}
/**
* Sets the total time value.
*
* @param activity the activity
* @param totalTime the total time
*/
public static void setTotalTimeValue(Activity activity, long totalTime) {
TextView totalTimeValue = activity.findViewById(R.id.stats_total_time_value);
totalTimeValue.setText(StringUtils.formatElapsedTime(totalTime));
}
/**
* Sets the trip statistics values.
*
* @param activity the activity for finding views
* @param tripStatistics the trip statistics
* @param trackIconValue the track icon value or null to hide the track icon spinner
*/
public static void setTripStatisticsValues(Activity activity, TripStatistics tripStatistics, String trackIconValue) {
boolean metricUnits = PreferencesUtils.isMetricUnits(activity);
boolean reportSpeed = PreferencesUtils.isReportSpeed(activity);
boolean isRecording = PreferencesUtils.isRecording(activity);
// Set total distance
{
double totalDistance = tripStatistics == null ? Double.NaN : tripStatistics.getTotalDistance();
Pair<String, String> parts = StringUtils.getDistanceParts(activity, totalDistance, metricUnits);
TextView distanceValue = activity.findViewById(R.id.stats_distance_value);
distanceValue.setText(parts.first);
TextView distanceUnit = activity.findViewById(R.id.stats_distance_unit);
distanceUnit.setText(parts.second);
}
// Set activity type
{
activity.findViewById(R.id.stats_activity_type_label).setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE);
Spinner spinner = activity.findViewById(R.id.stats_activity_type_icon);
spinner.setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE);
if (trackIconValue != null) {
TrackIconUtils.setIconSpinner(spinner, trackIconValue);
}
spinner.setEnabled(isRecording);
}
// Set time
if (tripStatistics != null) {
setTotalTimeValue(activity, tripStatistics.getTotalTime());
TextView movingTimeValue = activity.findViewById(R.id.stats_moving_time_value);
movingTimeValue.setText(StringUtils.formatElapsedTime(tripStatistics.getMovingTime()));
}
// Set average speed/pace
{
double speed = tripStatistics != null ? tripStatistics.getAverageSpeed() : Double.NaN;
TextView speedLabel = activity.findViewById(R.id.stats_average_speed_label);
speedLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(activity, speed, metricUnits, reportSpeed);
TextView speedValue = activity.findViewById(R.id.stats_average_speed_value);
speedValue.setText(parts.first);
TextView speedUnit = activity.findViewById(R.id.stats_average_speed_unit);
speedUnit.setText(parts.second);
}
// Set max speed/pace
{
double speed = tripStatistics == null ? Double.NaN : tripStatistics.getMaxSpeed();
TextView speedLabel = activity.findViewById(R.id.stats_max_speed_label);
speedLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(activity, speed, metricUnits, reportSpeed);
TextView speedValue = activity.findViewById(R.id.stats_max_speed_value);
speedValue.setText(parts.first);
TextView speedUnit = activity.findViewById(R.id.stats_max_speed_unit);
speedUnit.setText(parts.second);
}
// Set average speed/pace
{
double speed = tripStatistics != null ? tripStatistics.getAverageSpeed() : Double.NaN;
TextView speedLabel = activity.findViewById(R.id.stats_average_speed_label);
speedLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(activity, speed, metricUnits, reportSpeed);
TextView speedValue = activity.findViewById(R.id.stats_average_speed_value);
speedValue.setText(parts.first);
TextView speedUnit = activity.findViewById(R.id.stats_average_speed_unit);
speedUnit.setText(parts.second);
}
// Set moving speed/pace
{
double speed = tripStatistics != null ? tripStatistics.getAverageMovingSpeed() : Double.NaN;
TextView speedLabel = activity.findViewById(R.id.stats_moving_speed_label);
speedLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(activity, speed, metricUnits, reportSpeed);
TextView speedValue = activity.findViewById(R.id.stats_moving_speed_value);
speedValue.setText(parts.first);
TextView speedUnit = activity.findViewById(R.id.stats_moving_speed_unit);
speedUnit.setText(parts.second);
}
// Set elevation
{
boolean showElevation = PreferencesUtils.isShowStatsGradeElevation(activity);
View gradeElevationSeparator = activity.findViewById(R.id.stats_elevation_separator);
gradeElevationSeparator.setVisibility(showElevation ? View.VISIBLE : View.GONE);
View gradeElevationContainer = activity.findViewById(R.id.stats_elevation_container);
gradeElevationContainer.setVisibility(showElevation ? View.VISIBLE : View.GONE);
if (showElevation) {
{
double elevation = tripStatistics == null ? Double.NaN : tripStatistics.getMinElevation();
Pair<String, String> parts = StringUtils.formatElevation(activity, elevation, metricUnits);
TextView elevationValue = activity.findViewById(R.id.stats_elevation_min_value);
elevationValue.setText(parts.first);
TextView elevationUnit = activity.findViewById(R.id.stats_elevation_min_unit);
elevationUnit.setText(parts.second);
}
{
double elevation = tripStatistics == null ? Double.NaN : tripStatistics.getMaxElevation();
Pair<String, String> parts = StringUtils.formatElevation(activity, elevation, metricUnits);
TextView elevationValue = activity.findViewById(R.id.stats_elevation_max_value);
elevationValue.setText(parts.first);
TextView elevationUnit = activity.findViewById(R.id.stats_elevation_max_unit);
elevationUnit.setText(parts.second);
}
}
}
}
}
@@ -132,7 +132,7 @@ public class StringUtils {
/**
* Format a decimal number while removing trailing zeros of the decimal part (if present).
*/
static String formatDecimal(double value, int decimalPlaces) {
public static String formatDecimal(double value, int decimalPlaces) {
if (decimalPlaces < 1) {
return Long.toString(Math.round(value));
}
@@ -158,7 +158,7 @@ public class StringUtils {
* @param metricUnits true to use metric unit
* @return the formatted distance (or null) and it's unit as {@link Pair}
*/
static Pair<String, String> getDistanceParts(Context context, double distance, boolean metricUnits) {
public static Pair<String, String> getDistanceParts(Context context, double distance, boolean metricUnits) {
if (Double.isNaN(distance) || Double.isInfinite(distance)) {
return new Pair<>(null, context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet));
}
@@ -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;
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

+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" />
@@ -18,7 +18,6 @@ limitations under the License.
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/marker_detail_waypoint"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -69,33 +68,4 @@ limitations under the License.
</LinearLayout>
</FrameLayout>
</FrameLayout>
<ScrollView
android:id="@+id/marker_detail_statistics"
style="@style/StatsScrollView">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/marker_detail_statistics_name"
style="@style/TextMedium"
android:paddingTop="8dp" />
<TextView
android:id="@+id/marker_detail_statistics_location"
style="@style/TextSmall" />
<TextView
style="@style/TextSmall"
android:paddingTop="4dp"
android:paddingBottom="6dp"
android:text="@string/marker_detail_statistics_title" />
<include layout="@layout/stats_content" />
</LinearLayout>
</ScrollView>
</FrameLayout>
+1 -1
View File
@@ -41,7 +41,7 @@ limitations under the License.
<EditText
android:id="@+id/marker_edit_statistics_name"
style="@style/EditNameMarker"
android:drawableLeft="@drawable/ic_marker_yellow_pushpin"
android:drawableLeft="@drawable/ic_marker_blue_pushpin"
android:hint="@string/generic_name"
android:imeOptions="actionDone"
android:importantForAutofill="no" />
-1
View File
@@ -363,7 +363,6 @@ limitations under the License.
<LinearLayout style="@style/StatsLargeItemContainer">
<TextView
android:id="@+id/stats_sensor_heart_rate_label"
style="@style/StatsLargeLabel"
android:text="@string/sensor_state_heart_rate" />
+3 -35
View File
@@ -25,6 +25,9 @@
<string name="stats_show_on_lockscreen_while_recording_key" translatable="false">trackdetail_show_on_lockscreen_while_recording</string>
<bool name="stats_show_on_lockscreen_while_recording_default" translatable="false">false</bool>
<string name="stats_keep_screen_on_while_recording_key" translatable="false">trackdetail_keep_screen_on_while_recording</string>
<bool name="stats_keep_screen_on_while_recording_default" translatable="false">false</bool>
<string name="settings_reset_key" translatable="false">settingsReset</string>
<string name="settings_sensor_bluetooth_heart_rate_key" translatable="false">bluetoothSensor</string>
@@ -114,41 +117,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>
<string name="voice_frequency_default" translatable="false">@string/frequency_off</string>
<string name="frequency_off" translatable="false">0</string>
<string-array name="frequency_values">
<item>@string/frequency_off</item>
<item>1</item>
<item>2</item>
<item>5</item>
<item>10</item>
<item>15</item>
<item>30</item>
<item>60</item>
<item>-1</item>
<item>-5</item>
<item>-10</item>
<item>-25</item>
<item>-50</item>
<item>-100</item>
</string-array>
<string name="recording_distance_interval_key" translatable="false">recordingDistanceInterval</string>
<string name="recording_distance_interval_default" translatable="false">10</string>
<string-array name="recording_distance_interval_values">
@@ -0,0 +1,40 @@
<?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>
<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>
<string name="voice_frequency_default" translatable="false">@string/frequency_off</string>
<string name="frequency_off" translatable="false">0</string>
<string-array name="frequency_values">
<item>@string/frequency_off</item>
<item>1</item>
<item>2</item>
<item>5</item>
<item>10</item>
<item>15</item>
<item>30</item>
<item>60</item>
<item>-1</item>
<item>-5</item>
<item>-10</item>
<item>-25</item>
<item>-50</item>
<item>-100</item>
</string-array>
</resources>
+99
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.">
@@ -1105,6 +1194,13 @@ limitations under the License.
While recording, show statistics without unlocking the device.
</string>
<string name="settings_recording_keep_screen_on_while_recording_title" translation_description="In the 'Settings' page, the option to change the 'show on lockscreen' behavior while recording.">
Keep screen on
</string>
<string name="settings_recording_keep_screen_on_while_recording_summary" translation_description="In the 'Settings' page, the option to change the 'show on lockscreen' behavior while recording.">
While recording, keep the screen on.
</string>
<string name="settings_recording" translation_description="In the 'Settings' page, the option to change the recording settings.
Character limit: ~26 (1 line)">
@@ -1425,6 +1521,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.
+13 -13
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"
@@ -73,18 +74,17 @@ limitations under the License.
android:key="@string/stats_show_on_lockscreen_while_recording_key"
android:summary="@string/settings_recording_show_on_lockscreen_while_recording_summary"
android:title="@string/settings_recording_show_on_lockscreen_while_recording_title" />
<CheckBoxPreference
android:defaultValue="@bool/stats_keep_screen_on_while_recording_default"
android:key="@string/stats_keep_screen_on_while_recording_key"
android:summary="@string/settings_recording_keep_screen_on_while_recording_summary"
android:title="@string/settings_recording_keep_screen_on_while_recording_title" />
<ListPreference
android:defaultValue="@string/voice_frequency_default"
android:entryValues="@array/frequency_values"
android:key="@string/voice_frequency_key"
android:title="@string/menu_voice_frequency"
app:useSimpleSummaryProvider="true" />
<ListPreference
android:defaultValue="@string/split_frequency_default"
android:entryValues="@array/frequency_values"
android:key="@string/split_frequency_key"
android:title="@string/menu_split_frequency"
app:useSimpleSummaryProvider="true" />
<ListPreference
android:defaultValue="@string/track_name_default"
android:entries="@array/track_name_options"