Compare commits

..

32 Commits

Author SHA1 Message Date
Dennis Guse e77590a125 Release v3.2.10 2019-12-15 22:11:19 +01:00
Dennis Guse ca330b2d80 Cleanup. 2019-12-15 21:28:03 +01:00
Dennis Guse 20f1524701 Cleanup. 2019-12-15 17:09:44 +01:00
Dennis Guse 96b2a15643 Bugfix: import and export on Android 10 requires requesting (privacy-unfriendly) legacy behavior. 2019-12-15 16:50:13 +01:00
Dennis Guse ad59392e02 Fix travisCI. 2019-12-14 23:58:43 +01:00
Dennis Guse 29f92b887c Fixed TrackRecordingService.insertWaypoint() to allow deprecated behavior. 2019-12-14 23:55:04 +01:00
Dennis Guse 46328211c2 Added promotion sticker. 2019-12-14 23:55:04 +01:00
Dennis Guse 3760130d1f Github: add donation. 2019-12-14 23:42:07 +01:00
Dennis Guse 1094827fac TrackListActivity: fixed landscape layout. 2019-12-11 23:05:31 +01:00
Dennis Guse 36fcb44af5 TripRecordingService: removed unused trip statistics marker updater. 2019-12-11 23:05:31 +01:00
Dennis Guse a683fbac4b Device reboot: do not restart a not successfully ended track recording. 2019-12-11 23:05:31 +01:00
Dennis Guse 64ad9019d1 ChartView: is not configurable anymore. 2019-12-11 23:05:31 +01:00
Dennis Guse 1e80d5c8a8 ChartView: draw waypoints (again).
Functionality was (accidentally) removed in 5a264c5c53
2019-12-11 23:05:31 +01:00
Matthias Lisin ac765b1ee8 Revert duration hint to MAY_DUCK.
Pass OnAudioFocusChangeListener to abandonAudioFocus
2019-12-10 12:58:57 +01:00
Dennis Guse 850c4f0a9a TravisCI: disabled non-functioning installation of Android emulator. 2019-12-10 06:53:22 +01:00
Dennis Guse 4c32999752 Add Google Play link. 2019-12-10 06:50:58 +01:00
Dennis Guse 76823b167a TrackListActivity: removed delete all from action menu. 2019-12-08 13:12:15 +01:00
Dennis Guse 9994cfd260 Bugfix: NPE. 2019-12-07 13:37:06 +01:00
Dennis Guse 1750236474 Added automation for Google Play publication (using fastlane). 2019-12-06 22:26:40 +01:00
Dennis Guse fed6e31f9e Cleanup. 2019-12-06 08:00:20 +01:00
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
75 changed files with 2356 additions and 1984 deletions
+4
View File
@@ -0,0 +1,4 @@
# These are supported funding model platforms
liberapay: dennis.guse
custom: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
+6 -2
View File
@@ -73,8 +73,12 @@ captures/
.idea/workspace.xml
# Keystore files
# Uncomment the following line if you do not want to check your keystore files in.
#*.jks
*.jks
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
# Fastlane
Appfile
releasePlayStore/*
Gemfile.lock
+11 -10
View File
@@ -19,25 +19,26 @@ android:
# installing tools to start, then use `sdkmanager` below to get the rest
- tools
- build-tools-28.0.3
- android-28
- android-29
# Emulator Management: Create, Start and Wait
install:
- echo y | sdkmanager "platform-tools"
- echo y | sdkmanager --channel=4 "emulator"
- echo y | sdkmanager "system-images;android-$API;$EMU_FLAVOR;$ABI"
- echo no | avdmanager create avd --force -n test -k "system-images;android-$API;$EMU_FLAVOR;$ABI" -c 10M
- emulator -verbose -avd test -no-accel -no-snapshot -no-window -camera-back none -camera-front none -selinux permissive -qemu -m 2048 &
- android-wait-for-emulator
- adb shell input keyevent 82 &
#install:
# - echo y | sdkmanager "platform-tools"
# - echo y | sdkmanager --channel=4 "emulator"
# - echo y | sdkmanager "system-images;android-$API;$EMU_FLAVOR;$ABI"
# - echo no | avdmanager create avd --force -n test -k "system-images;android-$API;$EMU_FLAVOR;$ABI" -c 10M
# - emulator -verbose -avd test -no-accel -no-snapshot -no-window -camera-back none -camera-front none -selinux permissive -qemu -m 2048 &
# - android-wait-for-emulator
# - adb shell input keyevent 82 &
script:
- ./gradlew clean build
- ./gradlew connectedCheck
# - ./gradlew connectedCheck
sbefore_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
+23 -3
View File
@@ -1,6 +1,6 @@
# OpenTrack: Developer information
# OpenTracks: Developer information
## Create releases for FDroid
## Craft new releases
1. Get next _version code_ (master branch): `git rev-list HEAD --count master`
2. Decide on _version name_ (semantic versioning)
@@ -9,7 +9,27 @@
5. Create commit with all changes
6. Tag newly create commit with _version name_ (e.g., v3.2.1)
7. Push commits and tags to public repository
8. Add changelog to tag (Github releases)
8. Add changelog to tag (Github releases)
## F-Droid
Application id: de.dennisguse.opentracks
It if it properly tagged F-Droid takes care of building the app and publishing.
Usually, it takes two days before the update is published (build, signed, and published).
The recent build activity can be found [here](https://f-droid.org/wiki/index.php?title=Special:RecentChanges&days=7&from=&hidebots=0&hideanons=1&hideliu=1&limit=500)
## Google Play Store
Application id: de.dennisguse.opentracks.playstore
Requirement: `gem install fastlane`
1. Update store meta data (e.g., description and screenshots)
`fastlane supply --version_code $(git rev-list HEAD --count master) --skip_upload_apk`
(requires API key)
2. Build signed apk
(requires keystore)
3. Upload signed apk to [Google Play Console](https://play.google.com/apps/publish)
## Translations (localization)
+3
View File
@@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"
+24 -6
View File
@@ -2,9 +2,30 @@
_OpenTracks_ is a sport tracking application that completely respects your privacy.
<a href="https://f-droid.org/packages/de.dennisguse.opentracks">
<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="60">
</a>
<table>
<tr>
<th>Free</th>
<th>Paid</th>
<th>Donations</th>
</tr>
<tr>
<td>
<a href="https://f-droid.org/packages/de.dennisguse.opentracks">
<img alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="60" align="middle">
</a>
</td>
<td>
<a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" height="60" align="middle">
</a>
</td>
<td>
<a href="https://liberapay.com/dennis.guse/donate">
<img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" height="45" align="middle">
</a>
</td>
</tr>
</table>
## Features:
* __Tracking:__ track your sport and outdoor activities
@@ -26,9 +47,6 @@ _Please note:_
_Tracks_ does not contain a _map_ (as this would required Internet access).
To show your tracks on a map, a 3rd party application (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand) or [MAPS.ME](https://play.google.com/store/apps/details?id=com.mapswithme.maps.pro)) needs to be installed.
<a href="https://liberapay.com/dennis.guse/donate">
<img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" height="45">
</a>
## Screenshots
<div>
+5
View File
@@ -74,6 +74,11 @@ android {
release {
minifyEnabled false
}
releasePlayStore {
applicationIdSuffix ".playstore"
versionNameSuffix "-PlayStore"
}
}
applicationVariants.all { variant ->
+1 -1
View File
@@ -17,7 +17,7 @@
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

+1 -1
View File
@@ -43,7 +43,7 @@
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

+1 -1
View File
@@ -41,7 +41,7 @@
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+1 -1
View File
@@ -33,7 +33,7 @@
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

+1 -1
View File
@@ -39,7 +39,7 @@
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

+1 -1
View File
@@ -18,7 +18,7 @@
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

+1 -1
View File
@@ -21,7 +21,7 @@
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

+38
View File
@@ -0,0 +1,38 @@
# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
# https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
# https://docs.fastlane.tools/plugins/available-plugins
#
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
default_platform(:android)
platform :android do
desc "Runs all the tests"
lane :test do
gradle(task: "test")
end
desc "Submit a new Beta Build to Crashlytics Beta"
lane :beta do
gradle(task: "clean assembleRelease")
crashlytics
# sh "your_script.sh"
# You can also use other beta testing services here
end
desc "Deploy a new version to the Google Play"
lane :deploy do
gradle(task: "clean assembleRelease")
upload_to_play_store
end
end
@@ -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,11 @@
v3.2.10: OpenTracks
Changes:
- ChartView is not configurable anymore
- After reboot: removed automatic restart of an unfinished recording
Bugfix:
- Speech announcements use MAY_DUCK (again)
- ChartView shows markers (again)
- Fixed landscape layout of TrackListActivity
- Request legacy file API behavior (necessary for Android 10)
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.
Binary file not shown.
@@ -1,103 +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;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import de.dennisguse.opentracks.services.TrackRecordingService;
/**
* Tests for the BootReceiver.
*
* @author Youtao Liu
*/
@RunWith(AndroidJUnit4.class)
public class BootReceiverTest {
private static final String SERVICE_NAME = "de.dennisguse.opentracks.services.TrackRecordingService";
private Context context = InstrumentationRegistry.getInstrumentation().getContext();
/**
* Tests the behavior when receive notification which is the phone boot.
*/
@Test
public void testOnReceive_startService() {
// Make sure no TrackRecordingService
Intent stopIntent = new Intent(context, TrackRecordingService.class);
context.stopService(stopIntent);
Assert.assertFalse(isServiceExisted(context));
BootReceiver bootReceiver = new BootReceiver();
Intent intent = new Intent();
intent.setAction(Intent.ACTION_BOOT_COMPLETED);
bootReceiver.onReceive(context, intent);
// Check if the service is started
Assert.assertFalse(isServiceExisted(context));
}
/**
* Tests the behavior when receive notification which is not the phone boot.
*/
@Test
public void testOnReceive_noStartService() {
// Make sure no TrackRecordingService
Intent stopIntent = new Intent(context, TrackRecordingService.class);
context.stopService(stopIntent);
Assert.assertFalse(isServiceExisted(context));
BootReceiver bootReceiver = new BootReceiver();
Intent intent = new Intent();
intent.setAction(Intent.ACTION_BUG_REPORT);
bootReceiver.onReceive(context, intent);
// Check if the service is not started
Assert.assertFalse(isServiceExisted(context));
}
/**
* Checks if a service is started in a context.
*
* @param context the context for checking a service
*/
private boolean isServiceExisted(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
Assert.assertNotNull(activityManager);
List<ActivityManager.RunningServiceInfo> serviceList = activityManager.getRunningServices(Integer.MAX_VALUE);
for (int i = 0; i < serviceList.size(); i++) {
RunningServiceInfo serviceInfo = serviceList.get(i);
ComponentName componentName = serviceInfo.service;
if (componentName.getClassName().equals(SERVICE_NAME)) {
return true;
}
}
return false;
}
}
@@ -71,17 +71,12 @@ public class TrackRecordingServiceTest {
private Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils providerUtils;
private long trackId = Math.abs(new Random().nextLong());
private final long trackId = Math.abs(new Random().nextLong());
static Intent createStartIntent(Context context) {
return new Intent(context, TrackRecordingService.class);
}
static void updateAutoResumePrefs(Context context, int attempts, int timeoutMins) {
PreferencesUtils.setInt(context, R.string.auto_resume_track_current_retry_key, attempts);
PreferencesUtils.setString(context, R.string.auto_resume_track_timeout_key, "" + timeoutMins);
}
@Before
public void setUp() {
// Set up the mock content resolver
@@ -95,9 +90,6 @@ public class TrackRecordingServiceTest {
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().apply();
// Disable auto resume by default.
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, 0);
// Ensure that the database is empty before every test
providerUtils.deleteAllTracks(context);
}
@@ -128,124 +120,6 @@ public class TrackRecordingServiceTest {
Assert.assertNotNull(service);
}
@MediumTest
@Test
public void testResumeAfterReboot_shouldResume() throws Exception {
// Insert a dummy track and mark it as recording track.
createDummyTrack(trackId, System.currentTimeMillis(), true);
// Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_default)));
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// then
Assert.assertNotNull(service);
// We expect to resume the previous track.
Assert.assertTrue(service.isRecording());
Assert.assertEquals(trackId, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testResumeAfterReboot_simulateReboot() throws Exception {
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_default)));
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
// Simulate recording a track.
long id = service.startNewTrack();
Assert.assertTrue(service.isRecording());
Assert.assertEquals(id, service.getRecordingTrackId());
mServiceRule.unbindService();
Assert.assertEquals(id, PreferencesUtils.getRecordingTrackId(context));
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
// then
Assert.assertNotNull(service);
Assert.assertTrue(service.isRecording());
}
@MediumTest
@Test
public void testResumeAfterReboot_noRecordingTrack() throws Exception {
// Insert a dummy track and mark it as recording track.
createDummyTrack(trackId, System.currentTimeMillis(), false);
// Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_default)));
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// then
Assert.assertNotNull(service);
// We don't expect to resume the previous track, because it was stopped.
Assert.assertFalse(service.isRecording());
Assert.assertEquals(-1L, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testResumeAfterReboot_expiredTrack() throws Exception {
// Insert a dummy track last updated 20 min ago.
createDummyTrack(trackId, System.currentTimeMillis() - 1500 * 60 * 1000, true);
// Clear the number of attempts and set the timeout to 10 min.
updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_default)));
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
// then
Assert.assertNotNull(service);
// We don't expect to resume the previous track, because it has expired.
Assert.assertFalse(service.isRecording());
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testResumeAfterReboot_tooManyAttempts() throws Exception {
// Insert a dummy track.
createDummyTrack(trackId, System.currentTimeMillis(), true);
// Set the number of attempts to max.
updateAutoResumePrefs(context, TrackRecordingService.MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS, Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_default)));
// Start the service in "resume" mode (simulates the on-reboot action).
Intent startIntent = createStartIntent(context);
startIntent.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
mServiceRule.startService(startIntent);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(startIntent));
//then
Assert.assertNotNull(service);
// We don't expect to resume the previous track, because there were already too many attempts.
Assert.assertFalse(service.isRecording());
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testRecording_noTracks() throws Exception {
@@ -289,16 +163,18 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testStartNewTrack_alreadyRecording() throws Exception {
createDummyTrack(trackId, -1L, true);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
Assert.assertTrue(service.isRecording());
long trackId = service.getRecordingTrackId();
long newTrackId = service.startNewTrack();
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, newTrackId);
Assert.assertEquals(trackId, PreferencesUtils.getRecordingTrackId(context));
Assert.assertEquals(trackId, service.getRecordingTrackId());
service.endCurrentTrack();
}
@MediumTest
@@ -314,44 +190,7 @@ public class TrackRecordingServiceTest {
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testInsertStatisticsMarker_noRecordingTrack() throws Exception {
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertFalse(service.isRecording());
long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
Assert.assertEquals(-1L, waypointId);
}
@MediumTest
@Test
public void testInsertStatisticsMarker_validLocation() throws Exception {
createDummyTrack(trackId, -1L, true);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
Assert.assertTrue(service.isRecording());
Assert.assertFalse(service.isPaused());
insertLocation(service);
long waypointId1 = service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
Assert.assertNotEquals(-1L, waypointId1);
long waypointId2 = service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
Assert.assertNotEquals(-1L, waypointId2);
Waypoint wpt = providerUtils.getWaypoint(waypointId1);
Assert.assertEquals(context.getString(R.string.marker_statistics_icon_url), wpt.getIcon());
Assert.assertEquals(context.getString(R.string.marker_split_name_format, 0), wpt.getName());
Assert.assertEquals(WaypointType.STATISTICS, wpt.getType());
Assert.assertEquals(trackId, wpt.getTrackId());
Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation());
Assert.assertNotNull(wpt.getTripStatistics());
// TODO check the rest of the params.
// TODO: Check waypoint 2.
}
// NOTE: Do not use to create a track that is currently recording.
private void createDummyTrack(long id, long stopTime, boolean isRecording) {
Track dummyTrack = new Track();
dummyTrack.setId(id);
@@ -399,12 +238,12 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testInsertWaypointMarker_validWaypoint() throws Exception {
createDummyTrack(trackId, -1L, true);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
Assert.assertTrue(service.isRecording());
insertLocation(service);
long trackId = service.getRecordingTrackId();
long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
Assert.assertNotEquals(-1L, waypointId);
Waypoint wpt = providerUtils.getWaypoint(waypointId);
@@ -415,5 +254,7 @@ public class TrackRecordingServiceTest {
Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation());
Assert.assertNull(wpt.getTripStatistics());
service.endCurrentTrack();
}
}
@@ -74,9 +74,6 @@ public class TrackRecordingServiceTestLooper {
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().apply();
// Disable auto resume by default.
TrackRecordingServiceTest.updateAutoResumePrefs(context, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT, 0);
// Ensure that the database is empty before every test
providerUtils.deleteAllTracks(context);
}
@@ -151,27 +148,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 {
@@ -242,9 +218,7 @@ public class TrackRecordingServiceTestLooper {
location.setBearing(3.0f);
service.insertTrackPoint(location);
if (i % 10 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
} else if (i % 7 == 0) {
if (i % 7 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
}
}
+5 -36
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="3162"
android:versionName="v3.2.8"> <!-- For FDroid. -->
android:versionCode="3195"
android:versionName="v3.2.10"> <!-- For FDroid. -->
<!-- Permission to initialize services -->
<application
android:name=".Startup"
@@ -25,6 +25,7 @@ limitations under the License.
android:hardwareAccelerated="true"
android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:theme="@style/ThemeCustom">
<!-- Activities -->
@@ -83,9 +84,7 @@ limitations under the License.
<activity android:name=".TrackEditActivity" />
<activity
android:name=".TrackListActivity"
android:screenOrientation="userPortrait">
<activity android:name=".TrackListActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -101,7 +100,6 @@ limitations under the License.
<activity android:name=".settings.SettingsActivity" />
<!-- Services -->
<provider
android:name=".content.ShareContentProvider"
android:authorities="${applicationId}.content"
@@ -118,14 +116,6 @@ limitations under the License.
android:resource="@xml/file_paths" />
</provider>
<!-- Receivers -->
<receiver android:name=".BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<!-- Providers -->
<service
android:name=".services.TrackRecordingService"
android:enabled="true"
@@ -145,44 +135,23 @@ 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" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<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>
@@ -1,57 +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;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import de.dennisguse.opentracks.services.TrackRecordingService;
import static android.content.Intent.ACTION_BOOT_COMPLETED;
/**
* This class react to the BOOT_COMPLETED broadcast.
* <p>
* One example of a broadcast message that this class is interested in,
* is notification about the phone boot. We may want to resume a previously
* started tracking session if the phone crashed (hopefully not), or the user
* decided to swap the battery or some external event occurred which forced
* a phone reboot.
* <p>
* This class simply delegates to {@link TrackRecordingService} to make a
* decision whether to continue with the previous track (if any), or just
* abandon it.
*
* @author Bartlomiej Niechwiej
*/
public class BootReceiver extends BroadcastReceiver {
private static final String TAG = BootReceiver.class.getSimpleName();
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "BootReceiver.onReceive: " + intent.getAction());
if (ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Intent startIntent = new Intent(context, TrackRecordingService.class)
.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
context.startService(startIntent);
} else {
Log.w(TAG, "BootReceiver: unsupported action");
}
}
}
@@ -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);
@@ -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;
/**
@@ -74,8 +74,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
// Note that the key can be null
@@ -152,7 +151,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,13 +159,13 @@ 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);
//TODO also show latitude and longitude in list
double latitude = cursor.getDouble(latitudeIndex);
double longitude = cursor.getDouble(longitudeIndex);
@@ -246,7 +244,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (track != null && item.getItemId() == R.id.marker_list_insert_marker) {
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, track.getId());
@@ -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;
@@ -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());
@@ -55,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;
@@ -118,25 +117,31 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
});
}
};
/*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected.
*/
// Note that sharedPreferenceChangeListener cannot be an anonymous inner class. Anonymous inner class will get garbage collected.
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
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() {
@@ -227,11 +232,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);
@@ -242,6 +249,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();
@@ -413,22 +431,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);
@@ -164,7 +164,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Menu items
private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem;
private MenuItem deleteAllMenuItem;
// private MenuItem deleteAllMenuItem;
private final OnClickListener stopListener = new OnClickListener() {
@Override
@@ -371,7 +371,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
ActivityUtils.configureSearchWidget(this, searchMenuItem, trackController);
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
deleteAllMenuItem = menu.findItem(R.id.track_list_delete_all);
return super.onCreateOptionsMenu(menu);
}
@@ -425,9 +424,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
intent = IntentUtils.newIntent(this, MarkerListActivity.class);
startActivity(intent);
return true;
case R.id.track_list_delete_all:
deleteTracks(new long[]{-1L});
return true;
case R.id.track_list_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
@@ -495,9 +491,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
startGpsMenuItem.setIcon(isGpsStarted ? R.drawable.ic_gps_fixed_24dp : R.drawable.ic_gps_off_24dp);
}
}
if (deleteAllMenuItem != null) {
deleteAllMenuItem.setVisible(hasTrack && !isRecording);
}
}
/**
@@ -23,8 +23,6 @@ import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Path;
import androidx.annotation.VisibleForTesting;
import java.text.NumberFormat;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
@@ -203,7 +201,6 @@ class ChartValueSeries {
/**
* Gets the interval value.
*/
@VisibleForTesting
int getInterval() {
return interval;
}
@@ -218,7 +215,6 @@ class ChartValueSeries {
/**
* Gets the max marker value.
*/
@VisibleForTesting
int getMaxMarkerValue() {
return maxMarkerValue;
}
@@ -37,12 +37,14 @@ import androidx.core.view.GestureDetectorCompat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
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;
@@ -55,10 +57,10 @@ import de.dennisguse.opentracks.util.UnitConversions;
*/
public class ChartView extends View {
public static final float MEDIUM_TEXT_SIZE = 18f;
public static final float SMALL_TEXT_SIZE = 12f;
static final float MEDIUM_TEXT_SIZE = 18f;
static final float SMALL_TEXT_SIZE = 12f;
public static final int Y_AXIS_INTERVALS = 5;
static final int Y_AXIS_INTERVALS = 5;
public static final int NUM_SERIES = 6;
public static final int ELEVATION_SERIES = 0;
@@ -79,19 +81,24 @@ public class ChartView extends View {
private static final int SPACER = 4;
private static final int Y_AXIS_OFFSET = 16;
//TODO Determine from actual size of the used drawable
private static final float WAYPOINT_X_ANCHOR = 13f / 48f;
static {
X_FRACTION_FORMAT.setMaximumFractionDigits(1);
X_FRACTION_FORMAT.setMinimumFractionDigits(1);
}
private final ChartValueSeries[] series = new ChartValueSeries[NUM_SERIES];
private final ArrayList<double[]> chartData = new ArrayList<>();
private final ArrayList<Waypoint> waypoints = new ArrayList<>();
private final List<double[]> chartData = new ArrayList<>();
private final List<Waypoint> waypoints = new ArrayList<>();
private final ExtremityMonitor xExtremityMonitor = new ExtremityMonitor();
private final Paint axisPaint;
private final Paint xAxisMarkerPaint;
private final Paint gridPaint;
private final Paint markerPaint;
private final Drawable pointer;
private final Drawable waypointMarker;
private final int markerWidth;
private final int markerHeight;
private final Scroller scroller;
@@ -145,7 +152,7 @@ public class ChartView extends View {
@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) {
if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2f) < markerHeight) {
int minDistance = Integer.MAX_VALUE;
Waypoint nearestWaypoint = null;
synchronized (waypoints) {
@@ -255,7 +262,7 @@ public class ChartView extends View {
gridPaint.setAntiAlias(false);
gridPaint.setPathEffect(new DashPathEffect(new float[]{3, 2}, 0));
Paint markerPaint = new Paint();
markerPaint = new Paint();
markerPaint.setStyle(Style.STROKE);
markerPaint.setColor(context.getResources().getColor(android.R.color.darker_gray));
markerPaint.setAntiAlias(false);
@@ -263,12 +270,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);
waypointMarker = MarkerUtils.getDefaultPhoto(context);
markerWidth = waypointMarker.getIntrinsicWidth();
markerHeight = waypointMarker.getIntrinsicHeight();
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
scroller = new Scroller(context);
@@ -323,11 +327,10 @@ public class ChartView extends View {
*
* @param dataPoints an array of data points to be added
*/
public void addDataPoints(ArrayList<double[]> dataPoints) {
public void addDataPoints(List<double[]> dataPoints) {
synchronized (chartData) {
chartData.addAll(dataPoints);
for (int i = 0; i < dataPoints.size(); i++) {
double[] dataPoint = dataPoints.get(i);
for (double[] dataPoint : dataPoints) {
xExtremityMonitor.update(dataPoint[0]);
for (int j = 0; j < series.length; j++) {
if (!Double.isNaN(dataPoint[j + 1])) {
@@ -492,6 +495,7 @@ public class ChartView extends View {
clipToGraphArea(canvas);
drawDataSeries(canvas);
drawWaypoints(canvas);
drawGrid(canvas);
canvas.restore();
@@ -532,6 +536,33 @@ public class ChartView extends View {
}
}
/**
* Draws the waypoints.
*
* @param canvas the canvas
*/
private void drawWaypoints(Canvas canvas) {
synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) {
final Waypoint waypoint = waypoints.get(i);
if (waypoint.getLocation() == null) {
continue;
}
double xValue = getWaypointXValue(waypoint);
if (xValue > maxX) {
continue;
}
canvas.save();
float x = getX(getWaypointXValue(waypoint));
canvas.drawLine(x, topBorder + spacer + markerHeight / 2, x, topBorder + effectiveHeight, markerPaint);
canvas.translate(x - (float) (markerWidth * WAYPOINT_X_ANCHOR), topBorder + spacer);
waypointMarker.draw(canvas);
canvas.restore();
}
}
}
/**
* Draws the grid.
*
@@ -539,9 +570,9 @@ public class ChartView extends View {
*/
private void drawGrid(Canvas canvas) {
// X axis grid
ArrayList<Double> xAxisMarkerPositions = getXAxisMarkerPositions(getXAxisInterval());
for (int i = 0; i < xAxisMarkerPositions.size(); i++) {
int x = getX(xAxisMarkerPositions.get(i));
List<Double> xAxisMarkerPositions = getXAxisMarkerPositions(getXAxisInterval());
for (double position : xAxisMarkerPositions) {
int x = getX(position);
canvas.drawLine(x, topBorder, x, topBorder + effectiveHeight, gridPaint);
}
// Y axis grid
@@ -613,10 +644,10 @@ public class ChartView extends View {
canvas.drawText(label, x + effectiveWidth + spacer, y + yOffset, axisPaint);
double interval = getXAxisInterval();
ArrayList<Double> markerPositions = getXAxisMarkerPositions(interval);
NumberFormat numberFormat = interval < 1 ? X_FRACTION_FORMAT : X_NUMBER_FORMAT;
for (int i = 0; i < markerPositions.size(); i++) {
drawXAxisMarker(canvas, markerPositions.get(i), numberFormat, spacer + yOffset);
for (double markerPosition : getXAxisMarkerPositions(interval)) {
drawXAxisMarker(canvas, markerPosition, numberFormat, spacer + yOffset);
}
}
@@ -666,8 +697,8 @@ public class ChartView extends View {
/**
* Gets the x axis marker positions.
*/
private ArrayList<Double> getXAxisMarkerPositions(double interval) {
ArrayList<Double> markers = new ArrayList<>();
private List<Double> getXAxisMarkerPositions(double interval) {
List<Double> markers = new ArrayList<>();
markers.add(0d);
for (int i = 1; i * interval < maxX; i++) {
markers.add(i * interval);
@@ -777,8 +808,7 @@ public class ChartView extends View {
private void drawPaths() {
boolean[] hasMoved = new boolean[series.length];
for (int i = 0; i < chartData.size(); i++) {
double[] dataPoint = chartData.get(i);
for (double[] dataPoint : chartData) {
for (int j = 0; j < series.length; j++) {
double value = dataPoint[j + 1];
if (Double.isNaN(value)) {
@@ -40,17 +40,24 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
*/
public class TrackDataHub implements DataSourceListener {
public synchronized static TrackDataHub newInstance(Context context) {
return new TrackDataHub(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACK_POINTS);
}
/**
* Target number of track points displayed by the map overlay.
* We may display more than this number of points.
*/
@Deprecated
private static final int TARGET_DISPLAYED_TRACK_POINTS = 5000;
/**
* Maximum number of waypoints to displayed.
*/
@VisibleForTesting
@Deprecated
private static final int MAX_DISPLAYED_WAYPOINTS = 128;
private static final String TAG = TrackDataHub.class.getSimpleName();
private final Context context;
@@ -95,10 +102,6 @@ public class TrackDataHub implements DataSourceListener {
resetSamplingState();
}
public synchronized static TrackDataHub newInstance(Context context) {
return new TrackDataHub(context, new TrackDataManager(), new ContentProviderUtils(context), TARGET_DISPLAYED_TRACK_POINTS);
}
public void start() {
if (started) {
Log.i(TAG, "TrackDataHub already started, ignoring start.");
@@ -396,8 +399,7 @@ public class TrackDataHub implements DataSourceListener {
trackDataListener.clearWaypoints();
}
try (Cursor cursor = contentProviderUtils.getWaypointCursor(
selectedTrackId, -1L, MAX_DISPLAYED_WAYPOINTS)) {
try (Cursor cursor = contentProviderUtils.getWaypointCursor(selectedTrackId, -1L, MAX_DISPLAYED_WAYPOINTS)) {
if (cursor != null && cursor.moveToFirst()) {
do {
Waypoint waypoint = contentProviderUtils.createWaypoint(cursor);
@@ -466,12 +468,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();
@@ -188,6 +188,7 @@ public final class Waypoint {
}
public enum WaypointType {
WAYPOINT, STATISTICS
WAYPOINT,
STATISTICS //Always the first waypoint; only one can exist.
}
}
@@ -26,8 +26,7 @@ public class WaypointCreationRequest {
public final static WaypointCreationRequest DEFAULT_WAYPOINT = new WaypointCreationRequest(WaypointType.WAYPOINT, false);
public final static WaypointCreationRequest DEFAULT_STATISTICS = new WaypointCreationRequest(WaypointType.STATISTICS, false);
@Deprecated // TODO Do not create per
public final static WaypointCreationRequest DEFAULT_START_TRACK = new WaypointCreationRequest(WaypointType.STATISTICS, true);
private WaypointType type;
@@ -5,8 +5,8 @@ public final class SensorDataSet {
public static final float DATA_UNAVAILABLE = Float.NaN;
//TODO It might be necessary to consider: if sensor was connected as well.
public String sensorName;
public String sensorAddress;
private String sensorName;
private String sensorAddress;
private float heartRate;
private float cadence;
private float power;
@@ -31,6 +31,7 @@ import androidx.fragment.app.FragmentActivity;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity;
@@ -58,7 +59,7 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
private static final String STATE_CHART_VIEW_BY_DISTANCE_KEY = "chartViewByDistance";
private final ArrayList<double[]> pendingPoints = new ArrayList<>();
private final List<double[]> pendingPoints = new ArrayList<>();
private TrackDataHub trackDataHub;
@@ -103,7 +104,7 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
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(), chartByDistance);
chartView = new ChartView(getContext(), chartByDistance);
}
@Override
@@ -198,13 +199,6 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
}
}
@Override
public void onSegmentSplit(Location location) {
if (isResumed()) {
fillDataPoint(location, null);
}
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
@@ -264,9 +258,8 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
}
reportSpeed = speed;
chartView.setReportSpeed(reportSpeed);
boolean chartShowSpeed = PreferencesUtils.shouldChartShowSpeed(getActivity());
setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed);
setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed);
setSeriesEnabled(ChartView.SPEED_SERIES, reportSpeed);
setSeriesEnabled(ChartView.PACE_SERIES, !reportSpeed);
runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -303,26 +296,14 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
*/
private void checkChartSettings() {
boolean needUpdate = false;
if (setSeriesEnabled(ChartView.ELEVATION_SERIES, PreferencesUtils.shouldChartShowElevation(getActivity()))) {
if (setSeriesEnabled(ChartView.SPEED_SERIES, reportSpeed)) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.PACE_SERIES, !reportSpeed)) {
needUpdate = true;
}
boolean chartShowSpeed = PreferencesUtils.shouldChartShowSpeed(getActivity());
if (setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed)) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed)) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.POWER_SERIES, PreferencesUtils.shouldChartShowPower(getActivity()))) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.CADENCE_SERIES, PreferencesUtils.shouldChartShowCadence(getActivity()))) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.HEART_RATE_SERIES, PreferencesUtils.shouldChartShowHeartRate(getActivity()))) {
needUpdate = true;
}
if (needUpdate) {
chartView.postInvalidate();
}
@@ -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;
@@ -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,26 +63,17 @@ 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;
//TODO Initialize immediately and remove in onDestroy()
private TrackRecordingServiceConnection trackRecordingServiceConnection;
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 +82,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 +225,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
@@ -141,10 +238,66 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind();
if (trackRecordingServiceConnection != null) {
trackRecordingServiceConnection.unbind();
}
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 +307,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 +329,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 +348,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
lastLocation = null;
}
}
StatsUtils.setLocationValues(getActivity(), lastLocation, isSelectedTrackRecording());
setLocationValues();
}
}
});
@@ -229,7 +377,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void run() {
if (isResumed()) {
updateUi(getActivity());
updateUI();
}
}
});
@@ -244,7 +392,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override
public void run() {
if (isResumed()) {
updateUi(getActivity());
updateUI();
}
}
});
@@ -300,6 +448,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 +468,176 @@ 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);
}
}
}
@@ -146,7 +146,7 @@ public enum TrackFileFormat {
}
public TrackExporter newTrackExporter(Context context, Track[] tracks, TrackExporterListener trackExporterListener) {
return newKmzTrackExporter(context, this.newTrackWriter(context, tracks.length > 1), tracks, trackExporterListener, exportPhotos);
return newKmzTrackExporter(context, newTrackWriter(context, tracks.length > 1), tracks, trackExporterListener, exportPhotos);
}
public String getExtension() {
@@ -188,7 +188,7 @@ public enum TrackFileFormat {
public TrackExporter newTrackExporter(Context context, Track[] tracks, TrackExporterListener trackExporterListener) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
TrackWriter trackWriter = this.newTrackWriter(context, tracks.length > 1);
TrackWriter trackWriter = newTrackWriter(context, tracks.length > 1);
return new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener);
}
@@ -103,16 +103,8 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
}
directoryDisplayName = FileUtils.getPathDisplayName(trackFileFormat.getExtension());
//TODO (still needed?): getLastNonConfiguration instance returned ExportAsyncTask before
// Object retained = getLastNonConfigurationInstance();
// if (retained instanceof ExportAsyncTask) {
// exportAsyncTask = (ExportAsyncTask) retained;
// exportAsyncTask.setActivity(this);
// } else {
exportAsyncTask = new ExportAsyncTask(this, trackFileFormat, directory);
exportAsyncTask.execute();
// }
}
@Override
@@ -44,19 +44,16 @@ import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.CustomContentProvider;
import de.dennisguse.opentracks.content.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;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest;
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;
@@ -75,8 +72,6 @@ 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 = "RESUME_TRACK";
private static final int NOTIFICATION_ID = 123;
public static final double PAUSE_LATITUDE = 100.0;
@@ -84,8 +79,6 @@ public class TrackRecordingService extends Service {
// 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;
private static final String TAG = TrackRecordingService.class.getSimpleName();
// 1 minute in milliseconds
private static final long ONE_MINUTE = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
@@ -95,7 +88,6 @@ public class TrackRecordingService extends Service {
private Handler handler;
private LocationManagerConnector locationManagerConnector;
private PeriodicTaskExecutor voiceExecutor;
private PeriodicTaskExecutor splitExecutor;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceNotificationManager notificationManager;
private long recordingTrackId;
@@ -104,7 +96,6 @@ public class TrackRecordingService extends Service {
private int recordingDistanceInterval;
private int maxRecordingDistance;
private int recordingGpsAccuracy;
private int autoResumeTrackTimeout;
private long currentRecordingInterval;
// The following variables are set when recording:
@@ -126,14 +117,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)) {
@@ -155,12 +142,9 @@ public class TrackRecordingService extends Service {
if (PreferencesUtils.isKey(context, R.string.recording_gps_accuracy_key, key)) {
recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context);
}
if (PreferencesUtils.isKey(context, R.string.auto_resume_track_timeout_key, key)) {
autoResumeTrackTimeout = PreferencesUtils.getAutoResumeTrackTimeout(context);
}
}
};
private TripStatisticsUpdater markerTripStatisticsUpdater;
private WakeLock wakeLock;
private BluetoothRemoteSensorManager remoteSensorManager;
private Location lastLocation;
@@ -219,7 +203,6 @@ public class TrackRecordingService extends Service {
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);
@@ -247,7 +230,6 @@ public class TrackRecordingService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleStartCommand(intent, startId);
return START_STICKY;
}
@@ -272,12 +254,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 {
@@ -298,23 +274,14 @@ public class TrackRecordingService extends Service {
super.onDestroy();
}
/**
* Returns true if the service is recording.
*/
public boolean isRecording() {
return PreferencesUtils.isRecording(recordingTrackId);
}
/**
* Returns true if the current recording is paused.
*/
public boolean isPaused() {
return recordingTrackPaused;
}
/**
* Gets the trip statistics.
*/
public TripStatistics getTripStatistics() {
if (trackTripStatisticsUpdater == null) {
return null;
@@ -333,43 +300,19 @@ public class TrackRecordingService extends Service {
return -1L;
}
WaypointType waypointType = waypointCreationRequest.getType();
boolean isStatistics = waypointType == WaypointType.STATISTICS;
// Get name
String name;
if (waypointCreationRequest.getName() != null) {
name = waypointCreationRequest.getName();
} else {
int nextWaypointNumber = contentProviderUtils.getNextWaypointNumber(recordingTrackId, waypointType);
String name = waypointCreationRequest.getName();
if (name == null) {
int nextWaypointNumber = contentProviderUtils.getNextWaypointNumber(recordingTrackId, waypointCreationRequest.getType());
if (nextWaypointNumber == -1) {
nextWaypointNumber = 0;
}
name = getString(isStatistics ? R.string.marker_split_name_format : R.string.marker_name_format, nextWaypointNumber);
name = getString(R.string.marker_name_format, nextWaypointNumber);
}
// Get category
String category = waypointCreationRequest.getCategory() != null ? waypointCreationRequest.getCategory() : "";
String description = waypointCreationRequest.getDescription() != null ? waypointCreationRequest.getDescription() : "";
String icon = getString(R.string.marker_waypoint_icon_url);
// Get tripStatistics, description, and icon
TripStatistics tripStatistics;
String description;
String icon;
if (isStatistics) {
long now = System.currentTimeMillis();
markerTripStatisticsUpdater.updateTime(now);
tripStatistics = markerTripStatisticsUpdater.getTripStatistics();
markerTripStatisticsUpdater = new TripStatisticsUpdater(now);
description = new DescriptionGeneratorImpl(this).generateWaypointDescription(tripStatistics);
icon = getString(R.string.marker_statistics_icon_url);
} else {
tripStatistics = null;
description = waypointCreationRequest.getDescription() != null ? waypointCreationRequest.getDescription() : "";
//TODO Bundle icon?
icon = getString(R.string.marker_waypoint_icon_url);
}
// Get length and duration
double length;
long duration;
Location location = getLastValidTrackPointInCurrentSegment(recordingTrackId);
@@ -392,63 +335,11 @@ public class TrackRecordingService extends Service {
String photoUrl = waypointCreationRequest.getPhotoUrl() != null ? waypointCreationRequest.getPhotoUrl() : "";
// Insert waypoint
Waypoint waypoint = new Waypoint(name, description, category, icon, recordingTrackId, waypointType, length, duration, -1L, -1L, location, tripStatistics, photoUrl);
Waypoint waypoint = new Waypoint(name, description, category, icon, recordingTrackId, waypointCreationRequest.getType(), length, duration, -1L, -1L, location, null, photoUrl);
Uri uri = contentProviderUtils.insertWaypoint(waypoint);
return Long.parseLong(uri.getLastPathSegment());
}
/**
* Handles start command.
*
* @param intent the intent
* @param startId the start id
*/
private void handleStartCommand(Intent intent, int startId) {
// Check if the service is called to resume track (from phone reboot)
boolean resumeTrackRequested = intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false);
if (resumeTrackRequested && !shouldResumeTrack()) {
Log.i(TAG, "Stop resume track.");
updateRecordingState(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, true);
stopSelfResult(startId);
}
}
/**
* Returns true if should resume.
*/
private boolean shouldResumeTrack() {
Track track = contentProviderUtils.getTrack(recordingTrackId);
if (track == null) {
Log.d(TAG, "Not resuming. Track is null.");
return false;
}
int retries = PreferencesUtils.getAutoResumeTrackCurrentRetryDefault(this);
if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) {
Log.d(TAG, "Not resuming. Exceeded maximum retry attempts.");
return false;
}
PreferencesUtils.incrementAutoResumeTrackCurrentRetryDefault(this);
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(getResources().getString(R.string.auto_resume_track_timeout_always))) {
Log.d(TAG, "Resuming. Auto-resume track timeout set to always.");
return true;
}
if (track.getTripStatistics() == null) {
Log.d(TAG, "Not resuming. No trip statistics.");
return false;
}
long stopTime = track.getTripStatistics().getStopTime();
return stopTime > 0 && (System.currentTimeMillis() - stopTime) <= autoResumeTrackTimeout * ONE_MINUTE;
}
/**
* Starts a new track.
*
@@ -461,7 +352,6 @@ public class TrackRecordingService extends Service {
}
long now = System.currentTimeMillis();
trackTripStatisticsUpdater = new TripStatisticsUpdater(now);
markerTripStatisticsUpdater = new TripStatisticsUpdater(now);
// Insert a track
Track track = new Track();
@@ -470,7 +360,6 @@ public class TrackRecordingService extends Service {
// Update shared preferences
updateRecordingState(trackId, false);
PreferencesUtils.resetAutoResumeTrackCurrentRetryDefault(this);
// Update database
track.setId(trackId);
@@ -481,6 +370,8 @@ public class TrackRecordingService extends Service {
track.setIcon(TrackIconUtils.getIconValue(this, category));
track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics());
contentProviderUtils.updateTrack(track);
//TODO Do not use insertWaypoint
insertWaypoint(WaypointCreationRequest.DEFAULT_START_TRACK);
startRecording(true);
@@ -498,23 +389,11 @@ public class TrackRecordingService extends Service {
TripStatistics tripStatistics = track.getTripStatistics();
trackTripStatisticsUpdater = new TripStatisticsUpdater(tripStatistics.getStartTime());
long markerStartTime;
Waypoint waypoint = contentProviderUtils.getLastWaypoint(recordingTrackId, WaypointType.STATISTICS);
if (waypoint != null && waypoint.getTripStatistics() != null) {
markerStartTime = waypoint.getTripStatistics().getStopTime();
} else {
markerStartTime = tripStatistics.getStartTime();
}
markerTripStatisticsUpdater = new TripStatisticsUpdater(markerStartTime);
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
while (locationIterator.hasNext()) {
Location location = locationIterator.next();
trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
if (location.getTime() > markerStartTime) {
markerTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
}
}
} catch (RuntimeException e) {
Log.e(TAG, "RuntimeException", e);
@@ -549,8 +428,7 @@ public class TrackRecordingService extends Service {
}
/**
* Common code for starting a new track, resuming a track, or restarting after
* phone reboot.
* Common code for starting a new track, resuming a track, or restarting after phone reboot.
*
* @param trackStarted true if track is started, false if track is resumed
*/
@@ -566,7 +444,6 @@ public class TrackRecordingService extends Service {
// Restore periodic tasks
voiceExecutor.restore();
splitExecutor.restore();
}
/**
@@ -611,7 +488,7 @@ public class TrackRecordingService extends Service {
contentProviderUtils.updateTrack(track);
}
}
endRecording(true, trackId);
endRecording(true);
}
/**
@@ -639,7 +516,7 @@ public class TrackRecordingService extends Service {
insertLocation(track, pause, null);
}
endRecording(false, recordingTrackId);
endRecording(false);
notificationManager.updateContent(getString(R.string.generic_paused));
}
@@ -648,12 +525,10 @@ public class TrackRecordingService extends Service {
* Common code for ending a track or pausing a track.
*
* @param trackStopped true if track is stopped, false if track is paused
* @param trackId the track id
*/
private void endRecording(boolean trackStopped, long trackId) {
private void endRecording(boolean trackStopped) {
// Shutdown periodic tasks
voiceExecutor.shutdown();
splitExecutor.shutdown();
// Update instance variables
if (remoteSensorManager != null) {
@@ -830,7 +705,6 @@ public class TrackRecordingService extends Service {
Uri uri = contentProviderUtils.insertTrackPoint(location, track.getId());
long trackPointId = Long.parseLong(uri.getLastPathSegment());
trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
markerTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
updateRecordingTrack(track, trackPointId, LocationUtils.isValidLocation(location));
} catch (SQLiteException e) {
/*
@@ -840,7 +714,6 @@ public class TrackRecordingService extends Service {
Log.w(TAG, "SQLiteException", e);
}
voiceExecutor.update();
splitExecutor.update();
}
/**
@@ -1031,15 +904,7 @@ public class TrackRecordingService extends Service {
@Override
public SensorDataSet getSensorData() {
if (trackRecordingService.remoteSensorManager == null) {
Log.d(TAG, "remoteSensorManager is null.");
return null;
}
if (trackRecordingService.getSensorDataSet() == null) {
Log.d(TAG, "Sensor data set is null.");
return null;
}
return trackRecordingService.remoteSensorManager.getSensorDataSet();
return trackRecordingService.getSensorDataSet();
}
/**
@@ -48,6 +48,8 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
private static final String TAG = TrackRecordingServiceConnection.class.getSimpleName();
@Deprecated
//TODO Always the context of the owner of a service connection (i.e., let it be passed to every call).
private final Context context;
private final Runnable callback;
@@ -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;
@@ -34,7 +30,6 @@ import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TTSUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
@@ -47,22 +42,54 @@ 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_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "Failed to request audio focus.");
}
}
@Override
public void onDone(String utteranceId) {
int result = audioManager.abandonAudioFocus(null);
int result = audioManager.abandonAudioFocus(audioFocusChangeListener);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "Failed to relinquish audio focus.");
}
@@ -70,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);
@@ -103,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
@@ -129,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;
@@ -138,7 +149,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
synchronized (this) {
if (!ttsReady) {
ttsReady = initStatus == TextToSpeech.SUCCESS;
ttsReady = ttsInitStatus == TextToSpeech.SUCCESS;
if (ttsReady) {
onTtsReady();
}
@@ -149,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;
}
@@ -158,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;
/*
@@ -188,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, TTSUtils.getTTSStream(), 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;
@@ -256,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();
}
}
@@ -40,7 +40,7 @@ public class BluetoothLeAdapter extends BaseAdapter {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
//TODO Check if there is a better way to achieve identical look and feel to ListPreference.
//Use material design single choice; for old style use: android.R.layout.select_dialog_singlechoice
currentView = inflater.inflate(androidx.appcompat.R.layout.select_dialog_singlechoice_material, null);
currentView = inflater.inflate(androidx.appcompat.R.layout.select_dialog_singlechoice_material, parent);
}
Device device = devices.get(position);
@@ -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,7 +149,7 @@ 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();
@@ -142,22 +142,4 @@ final class PreferenceHelper {
return entries;
}
static String[] getAutoResumeTrackTimeoutEntries(Context context) {
String[] entryValues = context.getResources().getStringArray(R.array.auto_resume_track_timeout_values);
String[] entries = new String[entryValues.length];
for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]);
if (value == Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_never))) {
entries[i] = context.getString(R.string.value_never);
} else if (value == Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_always))) {
entries[i] = context.getString(R.string.value_always);
} else {
entries[i] = context.getString(R.string.value_integer_minute, value);
}
}
return entries;
}
}
@@ -73,14 +73,6 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
}
});
}
if (PreferencesUtils.isKey(getActivity(), R.string.stats_rate_key, key)) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
updateSpeed();
}
});
}
}
};
@@ -104,7 +96,6 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
public void onResume() {
super.onResume();
PreferencesUtils.getSharedPreferences(getActivity()).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
updateSpeed();
updateUnits();
updateReset();
updateBluetooth();
@@ -150,11 +141,6 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
resetPreference.setEnabled(!isRecording);
}
private void updateSpeed() {
Preference speedCheckBoxPreference = findPreference(getString(R.string.chart_show_speed_key));
speedCheckBoxPreference.setTitle(PreferencesUtils.isReportSpeed(getActivity()) ? R.string.stats_speed : R.string.stats_pace);
}
private void updateBluetooth() {
// Disable Bluetooth preference if device does not have Bluetooth
BluetoothLePreference bluetoothPreference = findPreference(getString(R.string.settings_sensor_bluetooth_heart_rate_key));
@@ -167,9 +153,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()));
@@ -182,9 +165,6 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
ListPreference recordingGpsAccuracy = findPreference(getString(R.string.recording_gps_accuracy_key));
recordingGpsAccuracy.setEntries(PreferenceHelper.getRecordingGpsAccuracyEntries(getActivity(), metricUnits));
ListPreference autoresumeTrack = findPreference(getString(R.string.auto_resume_track_timeout_key));
autoresumeTrack.setEntries(PreferenceHelper.getAutoResumeTrackTimeoutEntries(getActivity()));
ListPreference statsRatePreferences = findPreference(getString(R.string.stats_rate_key));
String[] entries = getResources().getStringArray(metricUnits ? R.array.stats_rate_metric_options : R.array.stats_rate_imperial_options);
statsRatePreferences.setEntries(entries);
@@ -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);
}
}
@@ -20,7 +20,6 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceManager;
import de.dennisguse.opentracks.R;
@@ -32,8 +31,6 @@ import de.dennisguse.opentracks.R;
*/
public class PreferencesUtils {
public static final String BLUETOOTH_SENSOR_DEFAULT = "";
private PreferencesUtils() {
}
@@ -43,29 +40,13 @@ public class PreferencesUtils {
@Deprecated
//NOTE: is at the moment still used to determine if a track is currently recorded; better ask the service directly.
//NOTE: This is also used to recover from a reboot, but this data should not be exposed to the whole application.
//NOTE: This was also used to recover from a reboot, but this data should not be exposed to the whole application.
public static final long RECORDING_TRACK_ID_DEFAULT = -1L;
public static long getRecordingTrackId(Context context) {
return PreferencesUtils.getLong(context, R.string.recording_track_id_key, RECORDING_TRACK_ID_DEFAULT);
}
@VisibleForTesting
public static final int AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT = 0;
public static int getAutoResumeTrackCurrentRetryDefault(Context context) {
return PreferencesUtils.getInt(context, R.string.auto_resume_track_current_retry_key, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT);
}
public static void resetAutoResumeTrackCurrentRetryDefault(Context context) {
PreferencesUtils.setInt(context, R.string.auto_resume_track_current_retry_key, PreferencesUtils.AUTO_RESUME_TRACK_CURRENT_RETRY_DEFAULT);
}
public static void incrementAutoResumeTrackCurrentRetryDefault(Context context) {
PreferencesUtils.setInt(context, R.string.auto_resume_track_current_retry_key, getAutoResumeTrackCurrentRetryDefault(context) + 1);
}
public static String getDefaultActivity(Context context) {
return PreferencesUtils.getString(context, R.string.default_activity_key, context.getString(R.string.default_activity_default));
}
@@ -88,7 +69,7 @@ public class PreferencesUtils {
* Compares if keyId and key belong to the same shared preference key.
*
* @param keyId The resource id of the key
* @param key
* @param key The key of the preference
* @return true if key == null or key belongs to keyId
*/
public static boolean isKey(Context context, int keyId, String key) {
@@ -211,33 +192,16 @@ public class PreferencesUtils {
editor.apply();
}
/**
* Returns true if metric units.
*
* @param context the context
*/
public static boolean isMetricUnits(Context context) {
final String STATS_UNIT = context.getString(R.string.stats_units_default);
return STATS_UNIT.equals(getString(context, R.string.stats_units_key, STATS_UNIT));
}
/**
* Returns true if the preferred rate is speed, false if the preferred rate is
* pace.
*
* @param context the context
*/
public static boolean isReportSpeed(Context context) {
final String STATS_RATE_DEFAULT = context.getString(R.string.stats_rate_default);
return STATS_RATE_DEFAULT.equals(getString(context, R.string.stats_rate_key, STATS_RATE_DEFAULT));
}
public static int getAutoResumeTrackTimeout(Context context) {
final int AUTO_RESUME_TRACK_TIMEOUT = Integer.parseInt(context.getResources().getString(R.string.auto_resume_track_timeout_default));
return PreferencesUtils.getInt(context, R.string.auto_resume_track_timeout_key, AUTO_RESUME_TRACK_TIMEOUT);
}
public static boolean isRecordingTrackPaused(Context context) {
return PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key, isRecordingTrackPausedDefault(context));
}
@@ -251,37 +215,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 shouldChartShowCadence(Context context) {
final boolean CHART_SHOW_CADENCE = context.getResources().getBoolean(R.bool.chart_show_cadence_default);
return PreferencesUtils.getBoolean(context, R.string.chart_show_cadence_key, CHART_SHOW_CADENCE);
}
public static boolean shouldChartShowElevation(Context context) {
final boolean CHART_SHOW_ELEVATION = context.getResources().getBoolean(R.bool.chart_show_elevation_default);
return PreferencesUtils.getBoolean(context, R.string.chart_show_elevation_key, CHART_SHOW_ELEVATION);
}
public static boolean shouldChartShowHeartRate(Context context) {
final boolean CHART_SHOW_HEARTRATE = context.getResources().getBoolean(R.bool.chart_show_heart_rate_default);
return PreferencesUtils.getBoolean(context, R.string.chart_show_heart_rate_key, CHART_SHOW_HEARTRATE);
}
public static boolean shouldChartShowPower(Context context) {
final boolean CHART_SHOW_POWER = context.getResources().getBoolean(R.bool.chart_show_power_default);
return PreferencesUtils.getBoolean(context, R.string.chart_show_power_key, CHART_SHOW_POWER);
}
public static boolean shouldChartShowSpeed(Context context) {
final boolean CHART_SHOW_SPEED = context.getResources().getBoolean(R.bool.chart_show_speed_default);
return PreferencesUtils.getBoolean(context, R.string.chart_show_speed_key, CHART_SHOW_SPEED);
return PreferencesUtils.getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothHeartRateSensorAddressNone(context));
}
public static boolean shouldShowStatsOnLockscreen(Context context) {
@@ -289,6 +236,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);
@@ -304,13 +256,7 @@ 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));
}
@@ -341,9 +287,6 @@ public class PreferencesUtils {
}
public static int getRecordingGPSAccuracy(Context context) {
final int RECORDING_GPS_ACCURACY = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_default));
return PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key, RECORDING_GPS_ACCURACY);
@@ -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));
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

+64 -57
View File
@@ -17,80 +17,87 @@ limitations under the License.
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal">
android:orientation="vertical">
<include layout="@layout/toolbar" />
<include layout="@layout/track_controller" />
<ListView
android:id="@+id/track_list"
android:layout_width="0dp"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1.0" />
android:baselineAligned="false"
android:orientation="horizontal">
<RelativeLayout
android:id="@+id/track_list_empty_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0">
<include layout="@layout/track_controller" />
<LinearLayout
android:id="@+id/track_list_arrow_long"
android:layout_width="wrap_content"
<ListView
android:id="@+id/track_list"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingRight="16dp">
android:layout_weight="1.0" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<RelativeLayout
android:id="@+id/track_list_empty_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0">
<ImageView
<LinearLayout
android:id="@+id/track_list_arrow_long"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="10"
android:contentDescription="@string/image_arrow"
android:src="@drawable/ic_arrow_long" />
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingRight="16dp">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="17" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toRightOf="@id/track_list_arrow_long"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="10"
android:contentDescription="@string/image_arrow"
android:src="@drawable/ic_arrow_long" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="4" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="17" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="24">
<TextView
style="@style/TextLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight=".7"
android:text="@string/track_list_empty_message"
android:textColor="@color/list_empty_text" />
android:layout_height="match_parent"
android:layout_toRightOf="@id/track_list_arrow_long"
android:orientation="vertical">
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight=".3" />
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="4" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="24">
<TextView
style="@style/TextLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight=".7"
android:text="@string/track_list_empty_message"
android:textColor="@color/list_empty_text" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight=".3" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
</LinearLayout>
</LinearLayout>
+23 -58
View File
@@ -17,85 +17,50 @@ limitations under the License.
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/marker_detail_waypoint"
<ImageView
android:id="@+id/marker_detail_waypoint_photo"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="@android:color/black"
android:contentDescription="@string/image_photo_marker"
android:scaleType="fitCenter" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- A gradient on top of the text -->
<ImageView
android:id="@+id/marker_detail_waypoint_photo"
android:id="@+id/marker_detail_waypoint_text_gradient"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:alpha=".6"
android:contentDescription="@string/image_photo_marker"
android:scaleType="fitCenter" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- A gradient on top of the text -->
<ImageView
android:id="@+id/marker_detail_waypoint_text_gradient"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha=".6"
android:contentDescription="@string/image_photo_marker"
android:src="@drawable/text_gradient" />
<LinearLayout
android:id="@+id/marker_detail_waypoint_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="80dp"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/marker_detail_waypoint_name"
style="@style/TextMedium"
android:paddingTop="8dp" />
<TextView
android:id="@+id/marker_detail_waypoint_category"
style="@style/TextSmall" />
<TextView
android:id="@+id/marker_detail_waypoint_description"
style="@style/TextSmall" />
<TextView
android:id="@+id/marker_detail_waypoint_location"
style="@style/TextSmall" />
</LinearLayout>
</FrameLayout>
</FrameLayout>
<ScrollView
android:id="@+id/marker_detail_statistics"
style="@style/StatsScrollView">
android:src="@drawable/text_gradient" />
<LinearLayout
android:id="@+id/marker_detail_waypoint_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="80dp"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/marker_detail_statistics_name"
android:id="@+id/marker_detail_waypoint_name"
style="@style/TextMedium"
android:paddingTop="8dp" />
<TextView
android:id="@+id/marker_detail_statistics_location"
android:id="@+id/marker_detail_waypoint_category"
style="@style/TextSmall" />
<TextView
style="@style/TextSmall"
android:paddingTop="4dp"
android:paddingBottom="6dp"
android:text="@string/marker_detail_statistics_title" />
android:id="@+id/marker_detail_waypoint_description"
style="@style/TextSmall" />
<include layout="@layout/stats_content" />
<TextView
android:id="@+id/marker_detail_waypoint_location"
style="@style/TextSmall" />
</LinearLayout>
</ScrollView>
</FrameLayout>
</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" />
+381 -1
View File
@@ -21,6 +21,386 @@ limitations under the License.
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/stats_content" />
<!-- Distance and activity type -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Distance -->
<LinearLayout style="@style/StatsLargeItemContainer">
<TextView
style="@style/StatsLargeLabel"
android:text="@string/stats_distance" />
<LinearLayout style="@style/StatsLargeValueContainer">
<TextView
android:id="@+id/stats_distance_value"
style="@style/StatsLargeValue"
android:value="@string/value_unknown" />
<TextView
android:id="@+id/stats_distance_unit"
style="@style/StatsUnit" />
</LinearLayout>
</LinearLayout>
<!-- Activity type -->
<include
android:id="@+id/stats_activity_type"
layout="@layout/stats_activity_type" />
</LinearLayout>
<!-- Total time and moving time -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Total time -->
<LinearLayout style="@style/StatsLargeItemContainer">
<TextView
style="@style/StatsLargeLabel"
android:text="@string/stats_total_time" />
<TextView
android:id="@+id/stats_total_time_value"
style="@style/StatsLargeValue" />
</LinearLayout>
<!-- Moving time -->
<LinearLayout style="@style/StatsLargeItemContainer">
<TextView
style="@style/StatsLargeLabel"
android:text="@string/stats_moving_time" />
<TextView
android:id="@+id/stats_moving_time_value"
style="@style/StatsLargeValue"
android:value="@string/value_unknown" />
</LinearLayout>
</LinearLayout>
<!-- Horizontal line -->
<View style="@style/StatsHorizontalLine" />
<!-- Speed/pace and average -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Speed/pace -->
<LinearLayout
android:id="@+id/stats_speed"
style="@style/StatsSmallItemContainer">
<TextView
android:id="@+id/stats_speed_label"
style="@style/StatsSmallLabel" />
<LinearLayout style="@style/StatsSmallValueContainer">
<TextView
android:id="@+id/stats_speed_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown" />
<TextView
android:id="@+id/stats_speed_unit"
style="@style/StatsUnit" />
</LinearLayout>
</LinearLayout>
<!-- Average -->
<LinearLayout style="@style/StatsSmallItemContainer">
<TextView
android:id="@+id/stats_average_speed_label"
style="@style/StatsSmallLabel" />
<LinearLayout style="@style/StatsSmallValueContainer">
<TextView
android:id="@+id/stats_average_speed_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown" />
<TextView
android:id="@+id/stats_average_speed_unit"
style="@style/StatsUnit" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- Speed max and moving -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Speed max -->
<LinearLayout style="@style/StatsSmallItemContainer">
<TextView
android:id="@+id/stats_max_speed_label"
style="@style/StatsSmallLabel" />
<LinearLayout style="@style/StatsSmallValueContainer">
<TextView
android:id="@+id/stats_max_speed_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown" />
<TextView
android:id="@+id/stats_max_speed_unit"
style="@style/StatsUnit" />
</LinearLayout>
</LinearLayout>
<!-- Speed moving -->
<LinearLayout style="@style/StatsSmallItemContainer">
<TextView
android:id="@+id/stats_moving_speed_label"
style="@style/StatsSmallLabel" />
<LinearLayout style="@style/StatsSmallValueContainer">
<TextView
android:id="@+id/stats_moving_speed_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown" />
<TextView
android:id="@+id/stats_moving_speed_unit"
style="@style/StatsUnit" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<View
android:id="@+id/stats_elevation_separator"
style="@style/StatsHorizontalLine" />
<LinearLayout
android:id="@+id/stats_elevation_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout style="@style/StatsSmallGroupContainer">
<!-- This is a placeholder -->
</LinearLayout>
<!-- Elevation -->
<LinearLayout style="@style/StatsSmallGroupContainer">
<TextView
style="@style/StatsSmallLabel"
android:text="@string/stats_elevation" />
<TableLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<!-- Current elevation -->
<TableRow
android:id="@+id/stats_elevation"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
style="@style/StatsSmallValueContainer"
android:layout_gravity="start|bottom">
<TextView
style="@style/StatsSmallValue"
android:visibility="invisible" />
<TextView
android:id="@+id/stats_elevation_current_label"
style="@style/StatsSmallLabel"
android:paddingStart="8dp"
android:paddingEnd="8dp" />
</LinearLayout>
<LinearLayout
style="@style/StatsSmallValueContainer"
android:layout_gravity="start|bottom">
<TextView
android:id="@+id/stats_elevation_current_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown" />
<TextView
android:id="@+id/stats_elevation_current_unit"
style="@style/StatsUnit" />
</LinearLayout>
</TableRow>
<!-- Minimal elevation -->
<TableRow
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
style="@style/StatsSmallValueContainer"
android:layout_gravity="start|bottom">
<TextView
android:id="@+id/stats_elevation_min_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown" />
<TextView
android:id="@+id/stats_elevation_min_unit"
style="@style/StatsUnit" />
</LinearLayout>
<LinearLayout
style="@style/StatsSmallValueContainer"
android:layout_gravity="start|bottom">
<TextView
style="@style/StatsSmallValue"
android:visibility="invisible" />
<TextView
style="@style/StatsSmallLabel"
android:layout_gravity="bottom"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/stats_min" />
</LinearLayout>
</TableRow>
<!-- Maximal elevation -->
<TableRow
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
style="@style/StatsSmallValueContainer"
android:layout_gravity="start|bottom">
<TextView
android:id="@+id/stats_elevation_max_value"
style="@style/StatsSmallValue" />
<TextView
android:id="@+id/stats_elevation_max_unit"
style="@style/StatsUnit" />
</LinearLayout>
<LinearLayout
style="@style/StatsSmallValueContainer"
android:layout_gravity="start|bottom">
<TextView
style="@style/StatsSmallValue"
android:visibility="invisible" />
<TextView
style="@style/StatsSmallLabel"
android:layout_gravity="bottom"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/stats_max" />
</LinearLayout>
</TableRow>
</TableLayout>
</LinearLayout>
</LinearLayout>
<!-- Horizontal line -->
<View
android:id="@+id/stats_coordinate_separator"
style="@style/StatsHorizontalLine" />
<!-- Latitude and longitude -->
<LinearLayout
android:id="@+id/stats_coordinate_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout style="@style/StatsSmallItemContainer">
<TextView
style="@style/StatsSmallLabel"
android:text="@string/stats_latitude" />
<TextView
android:id="@+id/stats_latitude_value"
style="@style/StatsSmallValue" />
</LinearLayout>
<LinearLayout style="@style/StatsSmallItemContainer">
<TextView
style="@style/StatsSmallLabel"
android:text="@string/stats_longitude" />
<TextView
android:id="@+id/stats_longitude_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown" />
</LinearLayout>
</LinearLayout>
<View
android:id="@+id/stats_sensor_separator"
style="@style/StatsHorizontalLine" />
<!-- Sensor data: heart rate-->
<LinearLayout
android:id="@+id/stats_sensor_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout style="@style/StatsLargeItemContainer">
<TextView
style="@style/StatsLargeLabel"
android:text="@string/sensor_state_heart_rate" />
<LinearLayout style="@style/StatsLargeValueContainer">
<TextView
android:id="@+id/stats_sensor_heart_rate_value"
style="@style/StatsLargeValue"
android:value="@string/value_unknown" />
<TextView
style="@style/StatsUnit"
android:text="@string/sensor_unit_beats_per_minute" />
</LinearLayout>
</LinearLayout>
<LinearLayout style="@style/StatsSmallGroupContainer">
<TextView
style="@style/StatsSmallLabel"
android:text="@string/settings_sensor_bluetooth_sensor" />
<TextView
android:id="@+id/stats_sensor_heart_rate_sensor_value"
style="@style/StatsTinyValue"
android:value="@string/value_unknown" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
-396
View File
@@ -1,396 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2013 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.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Distance and activity type -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Distance -->
<LinearLayout style="@style/StatsLargeItemContainer">
<TextView
style="@style/StatsLargeLabel"
android:text="@string/stats_distance" />
<LinearLayout style="@style/StatsLargeValueContainer">
<TextView
android:id="@+id/stats_distance_value"
style="@style/StatsLargeValue"
android:value="@string/value_unknown" />
<TextView
android:id="@+id/stats_distance_unit"
style="@style/StatsUnit" />
</LinearLayout>
</LinearLayout>
<!-- Activity type -->
<include
android:id="@+id/stats_activity_type"
layout="@layout/stats_activity_type" />
</LinearLayout>
<!-- Total time and moving time -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Total time -->
<LinearLayout style="@style/StatsLargeItemContainer">
<TextView
style="@style/StatsLargeLabel"
android:text="@string/stats_total_time" />
<TextView
android:id="@+id/stats_total_time_value"
style="@style/StatsLargeValue" />
</LinearLayout>
<!-- Moving time -->
<LinearLayout style="@style/StatsLargeItemContainer">
<TextView
style="@style/StatsLargeLabel"
android:text="@string/stats_moving_time" />
<TextView
android:id="@+id/stats_moving_time_value"
style="@style/StatsLargeValue"
android:value="@string/value_unknown" />
</LinearLayout>
</LinearLayout>
<!-- Horizontal line -->
<View style="@style/StatsHorizontalLine" />
<!-- Speed/pace and average -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Speed/pace -->
<LinearLayout
android:id="@+id/stats_speed"
style="@style/StatsSmallItemContainer">
<TextView
android:id="@+id/stats_speed_label"
style="@style/StatsSmallLabel" />
<LinearLayout style="@style/StatsSmallValueContainer">
<TextView
android:id="@+id/stats_speed_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown" />
<TextView
android:id="@+id/stats_speed_unit"
style="@style/StatsUnit" />
</LinearLayout>
</LinearLayout>
<!-- Average -->
<LinearLayout style="@style/StatsSmallItemContainer">
<TextView
android:id="@+id/stats_average_speed_label"
style="@style/StatsSmallLabel" />
<LinearLayout style="@style/StatsSmallValueContainer">
<TextView
android:id="@+id/stats_average_speed_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown" />
<TextView
android:id="@+id/stats_average_speed_unit"
style="@style/StatsUnit" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- Speed max and moving -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Speed max -->
<LinearLayout style="@style/StatsSmallItemContainer">
<TextView
android:id="@+id/stats_max_speed_label"
style="@style/StatsSmallLabel" />
<LinearLayout style="@style/StatsSmallValueContainer">
<TextView
android:id="@+id/stats_max_speed_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown" />
<TextView
android:id="@+id/stats_max_speed_unit"
style="@style/StatsUnit" />
</LinearLayout>
</LinearLayout>
<!-- Speed moving -->
<LinearLayout style="@style/StatsSmallItemContainer">
<TextView
android:id="@+id/stats_moving_speed_label"
style="@style/StatsSmallLabel" />
<LinearLayout style="@style/StatsSmallValueContainer">
<TextView
android:id="@+id/stats_moving_speed_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown" />
<TextView
android:id="@+id/stats_moving_speed_unit"
style="@style/StatsUnit" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<View
android:id="@+id/stats_elevation_separator"
style="@style/StatsHorizontalLine" />
<LinearLayout
android:id="@+id/stats_elevation_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout style="@style/StatsSmallGroupContainer">
<!-- This is a placeholder -->
</LinearLayout>
<!-- Elevation -->
<LinearLayout style="@style/StatsSmallGroupContainer">
<TextView
style="@style/StatsSmallLabel"
android:text="@string/stats_elevation" />
<TableLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<!-- Current elevation -->
<TableRow
android:id="@+id/stats_elevation"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
style="@style/StatsSmallValueContainer"
android:layout_gravity="start|bottom">
<TextView
style="@style/StatsSmallValue"
android:visibility="invisible" />
<TextView
android:id="@+id/stats_elevation_current_label"
style="@style/StatsSmallLabel"
android:paddingStart="8dp"
android:paddingEnd="8dp" />
</LinearLayout>
<LinearLayout
style="@style/StatsSmallValueContainer"
android:layout_gravity="start|bottom">
<TextView
android:id="@+id/stats_elevation_current_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown" />
<TextView
android:id="@+id/stats_elevation_current_unit"
style="@style/StatsUnit" />
</LinearLayout>
</TableRow>
<!-- Minimal elevation -->
<TableRow
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
style="@style/StatsSmallValueContainer"
android:layout_gravity="start|bottom">
<TextView
android:id="@+id/stats_elevation_min_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown" />
<TextView
android:id="@+id/stats_elevation_min_unit"
style="@style/StatsUnit" />
</LinearLayout>
<LinearLayout
style="@style/StatsSmallValueContainer"
android:layout_gravity="start|bottom">
<TextView
style="@style/StatsSmallValue"
android:visibility="invisible" />
<TextView
style="@style/StatsSmallLabel"
android:layout_gravity="bottom"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/stats_min" />
</LinearLayout>
</TableRow>
<!-- Maximal elevation -->
<TableRow
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
style="@style/StatsSmallValueContainer"
android:layout_gravity="start|bottom">
<TextView
android:id="@+id/stats_elevation_max_value"
style="@style/StatsSmallValue" />
<TextView
android:id="@+id/stats_elevation_max_unit"
style="@style/StatsUnit" />
</LinearLayout>
<LinearLayout
style="@style/StatsSmallValueContainer"
android:layout_gravity="start|bottom">
<TextView
style="@style/StatsSmallValue"
android:visibility="invisible" />
<TextView
style="@style/StatsSmallLabel"
android:layout_gravity="bottom"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/stats_max" />
</LinearLayout>
</TableRow>
</TableLayout>
</LinearLayout>
</LinearLayout>
<!-- Horizontal line -->
<View
android:id="@+id/stats_coordinate_separator"
style="@style/StatsHorizontalLine" />
<!-- Latitude and longitude -->
<LinearLayout
android:id="@+id/stats_coordinate_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout style="@style/StatsSmallItemContainer">
<TextView
style="@style/StatsSmallLabel"
android:text="@string/stats_latitude" />
<TextView
android:id="@+id/stats_latitude_value"
style="@style/StatsSmallValue" />
</LinearLayout>
<LinearLayout style="@style/StatsSmallItemContainer">
<TextView
style="@style/StatsSmallLabel"
android:text="@string/stats_longitude" />
<TextView
android:id="@+id/stats_longitude_value"
style="@style/StatsSmallValue"
android:value="@string/value_unknown" />
</LinearLayout>
</LinearLayout>
<View
android:id="@+id/stats_sensor_separator"
style="@style/StatsHorizontalLine" />
<!-- Sensor data: heart rate-->
<LinearLayout
android:id="@+id/stats_sensor_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout style="@style/StatsLargeItemContainer">
<TextView
android:id="@+id/stats_sensor_heart_rate_label"
style="@style/StatsLargeLabel"
android:text="@string/sensor_state_heart_rate" />
<LinearLayout style="@style/StatsLargeValueContainer">
<TextView
android:id="@+id/stats_sensor_heart_rate_value"
style="@style/StatsLargeValue"
android:value="@string/value_unknown" />
<TextView
style="@style/StatsUnit"
android:text="@string/sensor_unit_beats_per_minute" />
</LinearLayout>
</LinearLayout>
<LinearLayout style="@style/StatsSmallGroupContainer">
<TextView
style="@style/StatsSmallLabel"
android:text="@string/settings_sensor_bluetooth_sensor" />
<TextView
android:id="@+id/stats_sensor_heart_rate_sensor_value"
style="@style/StatsTinyValue"
android:value="@string/value_unknown" />
</LinearLayout>
</LinearLayout>
</merge>
-4
View File
@@ -31,10 +31,6 @@ limitations under the License.
android:icon="@drawable/ic_marker_show_24dp"
android:title="@string/menu_markers"
app:showAsAction="ifRoom" />
<item
android:id="@+id/track_list_delete_all"
android:icon="@drawable/ic_delete_forever_24dp"
android:title="@string/menu_delete_all" />
<item
android:id="@+id/track_list_settings"
android:icon="@drawable/ic_settings_24dp"
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
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.
-->
<resources>
<dimen name="widget_margin">0dp</dimen>
</resources>
+3 -49
View File
@@ -1,21 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Do not translate anything in this file directly. -->
<string name="chart_show_cadence_key" translatable="false">chartShowCadence</string>
<bool name="chart_show_cadence_default" translatable="false">true</bool>
<string name="chart_show_elevation_key" translatable="false">chartShowElevation</string>
<bool name="chart_show_elevation_default" translatable="false">true</bool>
<string name="chart_show_heart_rate_key" translatable="false">chartShowHeartRate</string>
<bool name="chart_show_heart_rate_default" translatable="false">true</bool>
<string name="chart_show_power_key" translatable="false">chartShowPower</string>
<bool name="chart_show_power_default" translatable="false">true</bool>
<string name="chart_show_speed_key" translatable="false">chartShowSpeed</string>
<bool name="chart_show_speed_default" translatable="false">true</bool>
<string name="stats_show_coordinate_key" translatable="false">statsShowCoordinate</string>
<bool name="stats_show_coordinate_default" translatable="false">false</bool>
@@ -25,6 +10,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>
@@ -102,40 +90,6 @@
<item>@string/activity_type_wind_surfing</item>
</string-array>
<string name="auto_resume_track_timeout_key" translatable="false">autoResumeTrackTimeout</string>
<string name="auto_resume_track_timeout_default" translatable="false">10</string>
<string name="auto_resume_track_timeout_always" translatable="false">-1</string>
<string name="auto_resume_track_timeout_never" translatable="false">0</string>
<string-array name="auto_resume_track_timeout_values">
<item>@string/auto_resume_track_timeout_never</item>
<item>5</item>
<item>@string/auto_resume_track_timeout_default</item>
<item>15</item>
<item>@string/auto_resume_track_timeout_always</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">
+56 -7
View File
@@ -1,18 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- 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">
<string name="chart_x_axis_key" translatable="false" tools:keep="@string/chart_x_axis_key">chartXAxis</string>
<string name="chart_x_axis_default" translatable="false" tools:keep="@string/chart_x_axis_default">@string/chart_x_axis_distance</string>
<string-array name="chart_x_axis_values" tools:keep="@array/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">
<string name="chart_x_axis_time" translatable="false" tools:keep="@string/chart_x_axis_time">TIME</string>
<string name="chart_x_axis_distance" translatable="false" tools:keep="@string/chart_x_axis_distance">DISTANCE</string>
<string-array name="chart_x_axis_options" tools:keep="@array/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" tools:keep="@string/split_frequency_key">splitFrequency</string>
<string name="split_frequency_default" translatable="false" tools:keep="@string/split_frequency_default">@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="chart_show_cadence_key" translatable="false">chartShowCadence</string>
<bool name="chart_show_cadence_default" translatable="false">true</bool>
<string name="chart_show_elevation_key" translatable="false">chartShowElevation</string>
<bool name="chart_show_elevation_default" translatable="false">true</bool>
<string name="chart_show_heart_rate_key" translatable="false">chartShowHeartRate</string>
<bool name="chart_show_heart_rate_default" translatable="false">true</bool>
<string name="chart_show_power_key" translatable="false">chartShowPower</string>
<bool name="chart_show_power_default" translatable="false">true</bool>
<string name="chart_show_speed_key" translatable="false">chartShowSpeed</string>
<bool name="chart_show_speed_default" translatable="false">true</bool>
<!-- <string name="auto_resume_track_timeout_key" translatable="false">autoResumeTrackTimeout</string>-->
<!-- <string name="auto_resume_track_timeout_default" translatable="false">10</string>-->
<!-- <string name="auto_resume_track_timeout_always" translatable="false">-1</string>-->
<!-- <string name="auto_resume_track_timeout_never" translatable="false">0</string>-->
<!-- <string-array name="auto_resume_track_timeout_values">-->
<!-- <item>@string/auto_resume_track_timeout_never</item>-->
<!-- <item>5</item>-->
<!-- <item>@string/auto_resume_track_timeout_default</item>-->
<!-- <item>15</item>-->
<!-- <item>@string/auto_resume_track_timeout_always</item>-->
<!-- </string-array>-->
</resources>
+7
View File
@@ -1194,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)">
+5 -28
View File
@@ -53,39 +53,23 @@ limitations under the License.
android:title="@string/settings_stats_coordinate" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/track_detail_chart_tab">
<CheckBoxPreference
android:defaultValue="true"
android:key="@string/chart_show_elevation_key"
android:title="@string/stats_elevation" />
<!-- title set in code -->
<CheckBoxPreference
android:defaultValue="true"
android:key="@string/chart_show_speed_key" />
<CheckBoxPreference
android:defaultValue="true"
android:key="@string/chart_show_heart_rate_key"
android:title="@string/sensor_state_heart_rate" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_recording">
<CheckBoxPreference
android:defaultValue="@bool/stats_show_on_lockscreen_while_recording_default"
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"
@@ -117,13 +101,6 @@ limitations under the License.
android:key="@string/recording_gps_accuracy_key"
android:title="@string/settings_recording_min_required_accuracy_title"
app:useSimpleSummaryProvider="true" />
<ListPreference
android:defaultValue="@string/auto_resume_track_timeout_default"
android:entries="@array/auto_resume_track_timeout_values"
android:entryValues="@array/auto_resume_track_timeout_values"
android:key="@string/auto_resume_track_timeout_key"
android:title="@string/settings_recording_auto_resume_track_timeout_title"
app:useSimpleSummaryProvider="true" />
<de.dennisguse.opentracks.settings.ActivityTypePreference
android:defaultValue="@string/default_activity_default"
android:key="@string/default_activity_key"