Compare commits

..

46 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
Dennis Guse c0672c41df Bugfix: crash if heart rate sensor was null (i.e., preference not initialized). 2019-11-29 23:18:49 +01:00
Dennis Guse 4c6601fbc5 Release v3.2.8 2019-11-29 22:58:22 +01:00
edern-appere b99c87d1bd Added French translation for Fdroid. 2019-11-29 22:46:43 +01:00
Dennis Guse 66ffbfe951 Fixed ChartFragment tests.
Initialization of looper was missing.
2019-11-29 22:43:51 +01:00
Dennis Guse 8cb01a1027 Bugfix: sharing a track was not adding the file(s).
While migrating to enable sharing multiple tracks a configuration option was missed.
2019-11-29 22:39:36 +01:00
Dennis Guse 193e5eccbb Cleanup. 2019-11-29 22:39:36 +01:00
Dennis Guse bf7fc28bb0 Bugfix: picture name suffixes were malformed on export and thus could not be re-imported. 2019-11-29 22:39:31 +01:00
Dennis Guse e35cbc343a Add pull request template. 2019-11-29 07:54:39 +01:00
Dennis Guse 1c7b053c8c Fixed MarkerDetailActivity.
The orientation of the layout was not set and thus adding the toolbar broke the UI.
2019-11-28 18:09:11 +01:00
Dennis Guse 88cdcd4b47 Use GestureDetector rather than doing it on our own. 2019-11-27 21:42:05 +01:00
Dennis Guse 1432fed8f9 Removed zoom controls for ChartFragment. 2019-11-27 21:41:53 +01:00
Dennis Guse a14307b9a4 Moved Chart* into own package. 2019-11-27 20:19:12 +01:00
Dennis Guse 3bdce5aa08 Cleanup: use autocloseable. 2019-11-25 23:09:08 +01:00
Dennis Guse d7526f93b8 Cleanup: removed ContentProviderUtils interface. 2019-11-25 23:02:48 +01:00
107 changed files with 3636 additions and 3452 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
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
name: Bug report name: Bug report
about: Report issues of Opentracks features about: Report issues of OpenTracks features
title: '' title: ''
labels: potential bug labels: potential bug
assignees: '' assignees: ''
+10
View File
@@ -0,0 +1,10 @@
# Thanks for your contribution.
**Describe the pull request**
A clear and concise description of what the pull request changes/adds.
**Link to the the issue**
(If available): The link to the issue that this pull request solves.
**License agreement**
By opening this pull request, you are providing your contribution under the _Apache License 2.0_ (see [LICENSE.md](LICENSE.md)).
+6 -2
View File
@@ -73,8 +73,12 @@ captures/
.idea/workspace.xml .idea/workspace.xml
# Keystore files # 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 # External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild .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 # installing tools to start, then use `sdkmanager` below to get the rest
- tools - tools
- build-tools-28.0.3 - build-tools-28.0.3
- android-28 - android-29
# Emulator Management: Create, Start and Wait # Emulator Management: Create, Start and Wait
install: #install:
- echo y | sdkmanager "platform-tools" # - echo y | sdkmanager "platform-tools"
- echo y | sdkmanager --channel=4 "emulator" # - echo y | sdkmanager --channel=4 "emulator"
- echo y | sdkmanager "system-images;android-$API;$EMU_FLAVOR;$ABI" # - 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 # - 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 & # - 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 # - android-wait-for-emulator
- adb shell input keyevent 82 & # - adb shell input keyevent 82 &
script: script:
- ./gradlew clean build - ./gradlew clean build
- ./gradlew connectedCheck # - ./gradlew connectedCheck
sbefore_cache: sbefore_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/ - rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache: cache:
directories: directories:
- $HOME/.gradle/caches/ - $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` 1. Get next _version code_ (master branch): `git rev-list HEAD --count master`
2. Decide on _version name_ (semantic versioning) 2. Decide on _version name_ (semantic versioning)
@@ -9,7 +9,27 @@
5. Create commit with all changes 5. Create commit with all changes
6. Tag newly create commit with _version name_ (e.g., v3.2.1) 6. Tag newly create commit with _version name_ (e.g., v3.2.1)
7. Push commits and tags to public repository 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) ## 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. _OpenTracks_ is a sport tracking application that completely respects your privacy.
<a href="https://f-droid.org/packages/de.dennisguse.opentracks"> <table>
<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="60"> <tr>
</a> <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: ## Features:
* __Tracking:__ track your sport and outdoor activities * __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). _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. 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 ## Screenshots
<div> <div>
+5
View File
@@ -74,6 +74,11 @@ android {
release { release {
minifyEnabled false minifyEnabled false
} }
releasePlayStore {
applicationIdSuffix ".playstore"
versionNameSuffix "-PlayStore"
}
} }
applicationVariants.all { variant -> applicationVariants.all { variant ->
+1 -1
View File
@@ -17,7 +17,7 @@
<rdf:RDF> <rdf:RDF>
<cc:Work rdf:about=""> <cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format> <dc:format>image/svg+xml</dc:format>
<dc:title></dc:title> <dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work> </cc:Work>
</rdf:RDF> </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> <rdf:RDF>
<cc:Work rdf:about=""> <cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format> <dc:format>image/svg+xml</dc:format>
<dc:title></dc:title> <dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work> </cc:Work>
</rdf:RDF> </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> <rdf:RDF>
<cc:Work rdf:about=""> <cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format> <dc:format>image/svg+xml</dc:format>
<dc:title></dc:title> <dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work> </cc:Work>
</rdf:RDF> </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> <rdf:RDF>
<cc:Work rdf:about=""> <cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format> <dc:format>image/svg+xml</dc:format>
<dc:title></dc:title> <dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work> </cc:Work>
</rdf:RDF> </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> <rdf:RDF>
<cc:Work rdf:about=""> <cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format> <dc:format>image/svg+xml</dc:format>
<dc:title></dc:title> <dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work> </cc:Work>
</rdf:RDF> </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> <rdf:RDF>
<cc:Work rdf:about=""> <cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format> <dc:format>image/svg+xml</dc:format>
<dc:title></dc:title> <dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work> </cc:Work>
</rdf:RDF> </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> <rdf:RDF>
<cc:Work rdf:about=""> <cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format> <dc:format>image/svg+xml</dc:format>
<dc:title></dc:title> <dc:title />
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work> </cc:Work>
</rdf:RDF> </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.8: OpenTracks
Changes:
- Chart: zoom via gestures
Bugfixes:
- Export via KMZ did not export picture filenames correctly
- Sharing multiple tracks did not include KMZ files
- MarkerDetailActivity's UI was not shown
- Settings crashed if preference of heart rate was not initialized
@@ -0,0 +1,10 @@
v3.2.9: OpenTracks
Changes:
- Option to keep screen on while recording
- Markers: removed split markers completely
- Voice announcement now work without READ_PHONE_STATE permission
Bugfixes:
- Statistics: heart rate was not shown
- Statistics: time was briefly reset to 0s
@@ -0,0 +1,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

@@ -0,0 +1,9 @@
OpenTracks est une application de suivi d'activités sportives qui respecte totalement votre vie privée.
Fonctionnalités :
* Suivi d'activité : enregistre vos activités sportives et de plein air
* Photos et repères : ajout de photos et de repères pendant l'enregistrement de l'activité
* Rythme cardiaque : support des capteurs Bluetooth LE
* Export : export des traces au format KML ou GPX
* Pas d'accès internet requis : Internet n'est pas nécessaire pour le fonctionnement de l'application
@@ -0,0 +1 @@
OpenTracks est une application de suivi d'activités sportives qui respecte totalement votre vie privée.
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;
}
}
@@ -13,7 +13,7 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package de.dennisguse.opentracks; package de.dennisguse.opentracks.chart;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -23,6 +23,8 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
/** /**
* Tests {@link ChartValueSeries}. * Tests {@link ChartValueSeries}.
* *
@@ -35,21 +35,19 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationFactory;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.content.Waypoint.WaypointType; import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
* A unit test for {@link ContentProviderUtilsImpl}. * A unit test for {@link ContentProviderUtils}.
* *
* @author Bartlomiej Niechwiej * @author Bartlomiej Niechwiej
* @author Youtao Liu * @author Youtao Liu
*/ */
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class CustomContentProviderUtilsImplTest { public class CustomContentProviderUtilsTest {
private static final String NAME_PREFIX = "test name"; private static final String NAME_PREFIX = "test name";
private static final String MOCK_DESC = "Mock Next Waypoint Desc!"; private static final String MOCK_DESC = "Mock Next Waypoint Desc!";
private static final String TEST_DESC = "Test Desc!"; private static final String TEST_DESC = "Test Desc!";
@@ -69,13 +67,13 @@ public class CustomContentProviderUtilsImplTest {
@Before @Before
public void setUp() { public void setUp() {
providerUtils = ContentProviderUtils.Factory.get(context); providerUtils = new ContentProviderUtils(context);
providerUtils.deleteAllTracks(context); providerUtils.deleteAllTracks(context);
} }
@Test @Test
public void testLocationIterator_noPoints() { public void testLocationIterator_noPoints() {
testIterator(1, 0, 1, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(1, 0, 1, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
} }
@Test @Test
@@ -105,36 +103,36 @@ public class CustomContentProviderUtilsImplTest {
@Test @Test
public void testLocationIterator_noBatchAscending() { public void testLocationIterator_noBatchAscending() {
testIterator(1, 50, 100, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(1, 50, 100, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(2, 50, 50, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
} }
@Test @Test
public void testLocationIterator_noBatchDescending() { public void testLocationIterator_noBatchDescending() {
testIterator(1, 50, 100, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(1, 50, 100, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 50, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(2, 50, 50, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
} }
@Test @Test
public void testLocationIterator_batchAscending() { public void testLocationIterator_batchAscending() {
testIterator(1, 50, 11, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(1, 50, 11, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(2, 50, 25, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
} }
@Test @Test
public void testLocationIterator_batchDescending() { public void testLocationIterator_batchDescending() {
testIterator(1, 50, 11, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(1, 50, 11, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
testIterator(2, 50, 25, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(2, 50, 25, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
} }
@Test @Test
public void testLocationIterator_largeTrack() { public void testLocationIterator_largeTrack() {
testIterator(1, 20000, 2000, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); testIterator(1, 20000, 2000, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
} }
private List<Location> testIterator(long trackId, int numPoints, int batchSize, boolean descending, LocationFactory locationFactory) { private List<Location> testIterator(long trackId, int numPoints, int batchSize, boolean descending, LocationFactory locationFactory) {
long lastPointId = initializeTrack(trackId, numPoints); long lastPointId = initializeTrack(trackId, numPoints);
((ContentProviderUtilsImpl) providerUtils).setDefaultCursorBatchSize(batchSize); ((ContentProviderUtils) providerUtils).setDefaultCursorBatchSize(batchSize);
List<Location> locations = new ArrayList<Location>(numPoints); List<Location> locations = new ArrayList<Location>(numPoints);
try (LocationIterator it = providerUtils.getTrackPointLocationIterator(trackId, -1L, descending, locationFactory)) { try (LocationIterator it = providerUtils.getTrackPointLocationIterator(trackId, -1L, descending, locationFactory)) {
while (it.hasNext()) { while (it.hasNext()) {
@@ -173,7 +171,7 @@ public class CustomContentProviderUtilsImplTest {
// Load all inserted locations. // Load all inserted locations.
long lastPointId = -1; long lastPointId = -1;
int counter = 0; int counter = 0;
try (LocationIterator it = providerUtils.getTrackPointLocationIterator(id, -1L, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) { try (LocationIterator it = providerUtils.getTrackPointLocationIterator(id, -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
while (it.hasNext()) { while (it.hasNext()) {
it.next(); it.next();
lastPointId = it.getLocationId(); lastPointId = it.getLocationId();
@@ -189,7 +187,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#createTrack(Cursor)}. * Tests the method {@link ContentProviderUtils#createTrack(Cursor)}.
*/ */
@Test @Test
public void testCreateTrack() { public void testCreateTrack() {
@@ -216,7 +214,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#deleteAllTracks(Context)} * Tests the method {@link ContentProviderUtils#deleteAllTracks(Context)}
*/ */
@Test @Test
public void testDeleteAllTracks() { public void testDeleteAllTracks() {
@@ -245,7 +243,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#deleteTrack(Context, long)}. * Tests the method {@link ContentProviderUtils#deleteTrack(Context, long)}.
*/ */
@Test @Test
public void testDeleteTrack() { public void testDeleteTrack() {
@@ -280,7 +278,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getAllTracks()} * Tests the method {@link ContentProviderUtils#getAllTracks()}
*/ */
@Test @Test
public void testGetAllTracks() { public void testGetAllTracks() {
@@ -293,7 +291,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getLastTrack()} * Tests the method {@link ContentProviderUtils#getLastTrack()}
*/ */
@Test @Test
public void testGetLastTrack() { public void testGetLastTrack() {
@@ -303,7 +301,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getTrack(long)} * Tests the method {@link ContentProviderUtils#getTrack(long)}
*/ */
@Test @Test
public void testGetTrack() { public void testGetTrack() {
@@ -313,7 +311,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#updateTrack(Track)} * Tests the method {@link ContentProviderUtils#updateTrack(Track)}
*/ */
@Test @Test
public void testUpdateTrack() { public void testUpdateTrack() {
@@ -330,7 +328,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#createContentValues(Waypoint)}. * Tests the method {@link ContentProviderUtils#createContentValues(Waypoint)}.
*/ */
@Test @Test
public void testCreateContentValues_waypoint() { public void testCreateContentValues_waypoint() {
@@ -368,7 +366,7 @@ public class CustomContentProviderUtilsImplTest {
waypoint.setLocation(location); waypoint.setLocation(location);
providerUtils.insertWaypoint(waypoint); providerUtils.insertWaypoint(waypoint);
ContentProviderUtilsImpl contentProviderUtils = new ContentProviderUtilsImpl(contentResolverMock); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(contentResolverMock);
long waypointId = System.currentTimeMillis(); long waypointId = System.currentTimeMillis();
waypoint.setId(waypointId); waypoint.setId(waypointId);
@@ -381,7 +379,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#createWaypoint(Cursor)}. * Tests the method {@link ContentProviderUtils#createWaypoint(Cursor)}.
*/ */
@Test @Test
public void testCreateWaypoint() { public void testCreateWaypoint() {
@@ -416,7 +414,7 @@ public class CustomContentProviderUtilsImplTest {
/** /**
* Tests the method * Tests the method
* {@link ContentProviderUtilsImpl#deleteWaypoint(Context, long, DescriptionGenerator)} * {@link ContentProviderUtils#deleteWaypoint(Context, long, DescriptionGenerator)}
* when there is only one waypoint in the track. * when there is only one waypoint in the track.
*/ */
@Test @Test
@@ -437,7 +435,7 @@ public class CustomContentProviderUtilsImplTest {
@Override @Override
public String generateWaypointDescription(TripStatistics tripStatistics) { public String generateWaypointDescription(TripStatistics tripStatistics) {
return CustomContentProviderUtilsImplTest.MOCK_DESC; return CustomContentProviderUtilsTest.MOCK_DESC;
} }
@Override @Override
@@ -452,7 +450,7 @@ public class CustomContentProviderUtilsImplTest {
/** /**
* Tests the method * Tests the method
* {@link ContentProviderUtilsImpl#deleteWaypoint(Context, long, DescriptionGenerator)} * {@link ContentProviderUtils#deleteWaypoint(Context, long, DescriptionGenerator)}
* when there is more than one waypoint in the track. * when there is more than one waypoint in the track.
*/ */
@Test @Test
@@ -495,7 +493,7 @@ public class CustomContentProviderUtilsImplTest {
DescriptionGenerator descriptionGenerator = new DescriptionGenerator() { DescriptionGenerator descriptionGenerator = new DescriptionGenerator() {
@Override @Override
public String generateWaypointDescription(TripStatistics tripStatistics) { public String generateWaypointDescription(TripStatistics tripStatistics) {
return CustomContentProviderUtilsImplTest.MOCK_DESC; return CustomContentProviderUtilsTest.MOCK_DESC;
} }
@Override @Override
@@ -508,12 +506,12 @@ public class CustomContentProviderUtilsImplTest {
providerUtils.deleteWaypoint(context, waypoint1Id, descriptionGenerator); providerUtils.deleteWaypoint(context, waypoint1Id, descriptionGenerator);
Assert.assertNull(providerUtils.getWaypoint(waypoint1Id)); Assert.assertNull(providerUtils.getWaypoint(waypoint1Id));
Assert.assertEquals(CustomContentProviderUtilsImplTest.MOCK_DESC, providerUtils.getWaypoint(waypoint2Id) Assert.assertEquals(CustomContentProviderUtilsTest.MOCK_DESC, providerUtils.getWaypoint(waypoint2Id)
.getDescription()); .getDescription());
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getFirstWaypointId(long)}. * Tests the method {@link ContentProviderUtils#getFirstWaypointId(long)}.
*/ */
@Test @Test
public void testGetFirstWaypointId() { public void testGetFirstWaypointId() {
@@ -535,7 +533,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getNextWaypointNumber(long, WaypointType)}. * Tests the method {@link ContentProviderUtils#getNextWaypointNumber(long, WaypointType)}.
*/ */
@Test @Test
public void testGetNextWaypointNumber() { public void testGetNextWaypointNumber() {
@@ -594,8 +592,8 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#insertWaypoint(Waypoint)} and * Tests the method {@link ContentProviderUtils#insertWaypoint(Waypoint)} and
* {@link ContentProviderUtilsImpl#getWaypoint(long)}. * {@link ContentProviderUtils#getWaypoint(long)}.
*/ */
@Test @Test
public void testInsertAndGetWaypoint() { public void testInsertAndGetWaypoint() {
@@ -612,7 +610,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#updateWaypoint(Waypoint)}. * Tests the method {@link ContentProviderUtils#updateWaypoint(Waypoint)}.
*/ */
@Test @Test
public void testUpdateWaypoint() { public void testUpdateWaypoint() {
@@ -634,7 +632,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#bulkInsertTrackPoint(Location[], int, long)}. * Tests the method {@link ContentProviderUtils#bulkInsertTrackPoint(Location[], int, long)}.
*/ */
@Test @Test
public void testBulkInsertTrackPoint() { public void testBulkInsertTrackPoint() {
@@ -650,7 +648,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#createTrackPoint(Cursor)}. * Tests the method {@link ContentProviderUtils#createTrackPoint(Cursor)}.
*/ */
@Test @Test
public void testCreateTrackPoint() { public void testCreateTrackPoint() {
@@ -710,7 +708,7 @@ public class CustomContentProviderUtilsImplTest {
/** /**
* Tests the method * Tests the method
* {@link ContentProviderUtilsImpl#insertTrackPoint(Location, long)}. * {@link ContentProviderUtils#insertTrackPoint(Location, long)}.
*/ */
@Test @Test
public void testInsertTrackPoint() { public void testInsertTrackPoint() {
@@ -724,7 +722,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getFirstTrackPointId(long)}. * Tests the method {@link ContentProviderUtils#getFirstTrackPointId(long)}.
*/ */
@Test @Test
public void testGetFirstTrackPointId() { public void testGetFirstTrackPointId() {
@@ -737,7 +735,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getLastTrackPointId(long)}. * Tests the method {@link ContentProviderUtils#getLastTrackPointId(long)}.
*/ */
@Test @Test
public void testGetLastTrackPointId() { public void testGetLastTrackPointId() {
@@ -750,7 +748,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getLastValidTrackPoint(long)}. * Tests the method {@link ContentProviderUtils#getLastValidTrackPoint(long)}.
*/ */
@Test @Test
public void testGetLastValidTrackPoint() { public void testGetLastValidTrackPoint() {
@@ -764,7 +762,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)} in descending. * Tests the method {@link ContentProviderUtils#getTrackPointCursor(long, long, int, boolean)} in descending.
*/ */
@Test @Test
public void testGetTrackPointCursor_desc() { public void testGetTrackPointCursor_desc() {
@@ -783,7 +781,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointCursor(long, long, int, boolean)} in ascending. * Tests the method {@link ContentProviderUtils#getTrackPointCursor(long, long, int, boolean)} in ascending.
*/ */
@Test @Test
public void testGetTrackPointCursor_asc() { public void testGetTrackPointCursor_asc() {
@@ -802,7 +800,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in descending. * Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in descending.
*/ */
@Test @Test
public void testGetTrackPointLocationIterator_desc() { public void testGetTrackPointLocationIterator_desc() {
@@ -818,7 +816,7 @@ public class CustomContentProviderUtilsImplTest {
long startTrackPointId = trackpointIds[9]; long startTrackPointId = trackpointIds[9];
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, true, LocationFactory.DEFAULT_LOCATION_FACTORY);
for (int i = 0; i < trackpointIds.length; i++) { for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext()); Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next(); Location location = locationIterator.next();
@@ -829,7 +827,7 @@ public class CustomContentProviderUtilsImplTest {
} }
/** /**
* Tests the method {@link ContentProviderUtilsImpl#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in ascending. * Tests the method {@link ContentProviderUtils#getTrackPointLocationIterator(long, long, boolean, LocationFactory)} in ascending.
*/ */
@Test @Test
public void testGetTrackPointLocationIterator_asc() { public void testGetTrackPointLocationIterator_asc() {
@@ -845,7 +843,7 @@ public class CustomContentProviderUtilsImplTest {
long startTrackPointId = trackpointIds[0]; long startTrackPointId = trackpointIds[0];
LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY); LocationIterator locationIterator = providerUtils.getTrackPointLocationIterator(trackId, startTrackPointId, false, LocationFactory.DEFAULT_LOCATION_FACTORY);
for (int i = 0; i < trackpointIds.length; i++) { for (int i = 0; i < trackpointIds.length; i++) {
Assert.assertTrue(locationIterator.hasNext()); Assert.assertTrue(locationIterator.hasNext());
Location location = locationIterator.next(); Location location = locationIterator.next();
@@ -892,7 +890,7 @@ public class CustomContentProviderUtilsImplTest {
/** /**
* Checks the value of a location. * Checks the value of a location.
* *
* @param i the index of this location which created in the method {@link CustomContentProviderUtilsImplTest#getTrack(long, int)} * @param i the index of this location which created in the method {@link CustomContentProviderUtilsTest#getTrack(long, int)}
* @param location the location to be checked * @param location the location to be checked
*/ */
private void checkLocation(int i, Location location) { private void checkLocation(int i, Location location) {
@@ -58,7 +58,7 @@ public class SearchEngineTest {
@Before @Before
public void setUp() { public void setUp() {
providerUtils = ContentProviderUtils.Factory.get(context); providerUtils = new ContentProviderUtils(context);
engine = new SearchEngine(providerUtils); engine = new SearchEngine(providerUtils);
} }
@@ -17,17 +17,20 @@
package de.dennisguse.opentracks.fragments; package de.dennisguse.opentracks.fragments;
import android.location.Location; import android.location.Location;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.AfterClass;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import de.dennisguse.opentracks.ChartView;
import de.dennisguse.opentracks.TrackStubUtils; import de.dennisguse.opentracks.TrackStubUtils;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.SensorDataSetLocation; import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
@@ -44,6 +47,17 @@ public class ChartFragmentTest {
private ChartFragment chartFragment; private ChartFragment chartFragment;
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
@AfterClass
public static void finalTearDown() {
if (Looper.myLooper() != null) Looper.myLooper().quit();
}
@Before @Before
public void setUp() { public void setUp() {
boolean chartByDistance = false; boolean chartByDistance = false;
@@ -71,17 +71,12 @@ public class TrackRecordingServiceTest {
private Context context = ApplicationProvider.getApplicationContext(); private Context context = ApplicationProvider.getApplicationContext();
private ContentProviderUtils providerUtils; 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) { static Intent createStartIntent(Context context) {
return new Intent(context, TrackRecordingService.class); 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 @Before
public void setUp() { public void setUp() {
// Set up the mock content resolver // Set up the mock content resolver
@@ -89,15 +84,12 @@ public class TrackRecordingServiceTest {
}; };
customContentProvider.attachInfo(context, null); customContentProvider.attachInfo(context, null);
providerUtils = ContentProviderUtils.Factory.get(context); providerUtils = new ContentProviderUtils(context);
// Let's use default values. // Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().apply(); 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 // Ensure that the database is empty before every test
providerUtils.deleteAllTracks(context); providerUtils.deleteAllTracks(context);
} }
@@ -128,124 +120,6 @@ public class TrackRecordingServiceTest {
Assert.assertNotNull(service); 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 @MediumTest
@Test @Test
public void testRecording_noTracks() throws Exception { public void testRecording_noTracks() throws Exception {
@@ -289,16 +163,18 @@ public class TrackRecordingServiceTest {
@MediumTest @MediumTest
@Test @Test
public void testStartNewTrack_alreadyRecording() throws Exception { public void testStartNewTrack_alreadyRecording() throws Exception {
createDummyTrack(trackId, -1L, true);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context))); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
Assert.assertTrue(service.isRecording()); Assert.assertTrue(service.isRecording());
long trackId = service.getRecordingTrackId();
long newTrackId = service.startNewTrack(); long newTrackId = service.startNewTrack();
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, newTrackId); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, newTrackId);
Assert.assertEquals(trackId, PreferencesUtils.getRecordingTrackId(context)); Assert.assertEquals(trackId, PreferencesUtils.getRecordingTrackId(context));
Assert.assertEquals(trackId, service.getRecordingTrackId()); Assert.assertEquals(trackId, service.getRecordingTrackId());
service.endCurrentTrack();
} }
@MediumTest @MediumTest
@@ -314,44 +190,7 @@ public class TrackRecordingServiceTest {
Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId()); Assert.assertEquals(PreferencesUtils.RECORDING_TRACK_ID_DEFAULT, service.getRecordingTrackId());
} }
@MediumTest // NOTE: Do not use to create a track that is currently recording.
@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.
}
private void createDummyTrack(long id, long stopTime, boolean isRecording) { private void createDummyTrack(long id, long stopTime, boolean isRecording) {
Track dummyTrack = new Track(); Track dummyTrack = new Track();
dummyTrack.setId(id); dummyTrack.setId(id);
@@ -399,12 +238,12 @@ public class TrackRecordingServiceTest {
@MediumTest @MediumTest
@Test @Test
public void testInsertWaypointMarker_validWaypoint() throws Exception { public void testInsertWaypointMarker_validWaypoint() throws Exception {
createDummyTrack(trackId, -1L, true);
ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context))); ITrackRecordingService service = ((ITrackRecordingService) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
Assert.assertTrue(service.isRecording()); Assert.assertTrue(service.isRecording());
insertLocation(service); insertLocation(service);
long trackId = service.getRecordingTrackId();
long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT); long waypointId = service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
Assert.assertNotEquals(-1L, waypointId); Assert.assertNotEquals(-1L, waypointId);
Waypoint wpt = providerUtils.getWaypoint(waypointId); Waypoint wpt = providerUtils.getWaypoint(waypointId);
@@ -415,5 +254,7 @@ public class TrackRecordingServiceTest {
Assert.assertEquals(0.0, wpt.getLength(), 0.01); Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation()); Assert.assertNotNull(wpt.getLocation());
Assert.assertNull(wpt.getTripStatistics()); Assert.assertNull(wpt.getTripStatistics());
service.endCurrentTrack();
} }
} }
@@ -68,15 +68,12 @@ public class TrackRecordingServiceTestLooper {
}; };
customContentProvider.attachInfo(context, null); customContentProvider.attachInfo(context, null);
providerUtils = ContentProviderUtils.Factory.get(context); providerUtils = new ContentProviderUtils(context);
// Let's use default values. // Let's use default values.
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
sharedPreferences.edit().clear().apply(); 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 // Ensure that the database is empty before every test
providerUtils.deleteAllTracks(context); providerUtils.deleteAllTracks(context);
} }
@@ -151,27 +148,6 @@ public class TrackRecordingServiceTestLooper {
fullRecordingSession(); 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 @MediumTest
@Test @Test
public void testWithProperties_metricUnitsDefault() throws TimeoutException { public void testWithProperties_metricUnitsDefault() throws TimeoutException {
@@ -242,9 +218,7 @@ public class TrackRecordingServiceTestLooper {
location.setBearing(3.0f); location.setBearing(3.0f);
service.insertTrackPoint(location); service.insertTrackPoint(location);
if (i % 10 == 0) { if (i % 7 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
} else if (i % 7 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT); service.insertWaypoint(WaypointCreationRequest.DEFAULT_WAYPOINT);
} }
} }
@@ -60,10 +60,15 @@ public class FileUtilsTest extends TestCase {
*/ */
public void testSanitizeFileName() { public void testSanitizeFileName() {
String name = "Swim\10ming-^across:/the/ pacific (ocean)."; String name = "Swim\10ming-^across:/the/ pacific (ocean).";
String expected = "Swim_ming-^across_the_ pacific (ocean)_"; String expected = "Swim_ming-^across_the_ pacific (ocean).";
assertEquals(expected, FileUtils.sanitizeFileName(name)); assertEquals(expected, FileUtils.sanitizeFileName(name));
} }
public void testSanitizeFileNameWithSuffix() {
String name = "FileName.jpeg";
assertEquals(name, FileUtils.sanitizeFileName(name));
}
/** /**
* Tests {@link FileUtils#sanitizeFileName(String)} with i18n characters (in Chinese and Russian). * Tests {@link FileUtils#sanitizeFileName(String)} with i18n characters (in Chinese and Russian).
* Verifies that they are allowed. * Verifies that they are allowed.
+5 -36
View File
@@ -16,8 +16,8 @@ limitations under the License.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.dennisguse.opentracks" package="de.dennisguse.opentracks"
android:installLocation="auto" android:installLocation="auto"
android:versionCode="3149" android:versionCode="3195"
android:versionName="v3.2.7"> <!-- For FDroid. --> android:versionName="v3.2.10"> <!-- For FDroid. -->
<!-- Permission to initialize services --> <!-- Permission to initialize services -->
<application <application
android:name=".Startup" android:name=".Startup"
@@ -25,6 +25,7 @@ limitations under the License.
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:icon="@drawable/ic_logo_color_24dp" android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/app_name" android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:theme="@style/ThemeCustom"> android:theme="@style/ThemeCustom">
<!-- Activities --> <!-- Activities -->
@@ -83,9 +84,7 @@ limitations under the License.
<activity android:name=".TrackEditActivity" /> <activity android:name=".TrackEditActivity" />
<activity <activity android:name=".TrackListActivity">
android:name=".TrackListActivity"
android:screenOrientation="userPortrait">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@@ -101,7 +100,6 @@ limitations under the License.
<activity android:name=".settings.SettingsActivity" /> <activity android:name=".settings.SettingsActivity" />
<!-- Services -->
<provider <provider
android:name=".content.ShareContentProvider" android:name=".content.ShareContentProvider"
android:authorities="${applicationId}.content" android:authorities="${applicationId}.content"
@@ -118,14 +116,6 @@ limitations under the License.
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<!-- Receivers -->
<receiver android:name=".BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<!-- Providers -->
<service <service
android:name=".services.TrackRecordingService" android:name=".services.TrackRecordingService"
android:enabled="true" android:enabled="true"
@@ -145,44 +135,23 @@ limitations under the License.
android:name="android.hardware.location.gps" android:name="android.hardware.location.gps"
android:required="true" /> android:required="true" />
<uses-feature <uses-feature
android:name="android.hardware.bluetooth" android:name="android.hardware.android.hardware.bluetooth_le"
android:required="false" /> android:required="false" />
<!-- Permissions to access sensors -->
<uses-feature <uses-feature
android:name="android.hardware.camera" android:name="android.hardware.camera"
android:required="false" /> 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 <uses-feature
android:name="android.hardware.sensor.compass" android:name="android.hardware.sensor.compass"
android:required="false" /> 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 <uses-feature
android:name="android.hardware.touchscreen" android:name="android.hardware.touchscreen"
android:required="false" /> 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.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <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.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest> </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");
}
}
}
@@ -69,7 +69,7 @@ public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override @Override
protected Boolean doInBackground(Void... params) { protected Boolean doInBackground(Void... params) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
if (trackIds.length == 1 && trackIds[0] == -1L) { if (trackIds.length == 1 && trackIds[0] == -1L) {
contentProviderUtils.deleteAllTracks(context); contentProviderUtils.deleteAllTracks(context);
@@ -61,7 +61,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
return; return;
} }
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(this); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
Waypoint waypoint = contentProviderUtils.getWaypoint(markerId); Waypoint waypoint = contentProviderUtils.getWaypoint(markerId);
markerIds = new ArrayList<>(); markerIds = new ArrayList<>();
@@ -71,8 +71,7 @@ public class MarkerEditActivity extends AbstractActivity {
waypointSection = findViewById(R.id.marker_edit_waypoint_section); waypointSection = findViewById(R.id.marker_edit_waypoint_section);
waypointName = findViewById(R.id.marker_edit_waypoint_name); waypointName = findViewById(R.id.marker_edit_waypoint_name);
waypointMarkerType = findViewById(R.id.marker_edit_waypoint_marker_type); waypointMarkerType = findViewById(R.id.marker_edit_waypoint_marker_type);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource( ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.waypoint_types, android.R.layout.simple_dropdown_item_1line);
this, R.array.waypoint_types, android.R.layout.simple_dropdown_item_1line);
waypointMarkerType.setAdapter(adapter); waypointMarkerType.setAdapter(adapter);
waypointDescription = findViewById(R.id.marker_edit_waypoint_description); waypointDescription = findViewById(R.id.marker_edit_waypoint_description);
@@ -127,7 +126,7 @@ public class MarkerEditActivity extends AbstractActivity {
if (newMarker) { if (newMarker) {
statisticsSection.setVisibility(View.GONE); statisticsSection.setVisibility(View.GONE);
waypointSection.setVisibility(View.VISIBLE); waypointSection.setVisibility(View.VISIBLE);
int nextWaypointNumber = trackId == -1L ? -1 : ContentProviderUtils.Factory.get(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT); int nextWaypointNumber = trackId == -1L ? -1 : new ContentProviderUtils(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
if (nextWaypointNumber == -1) { if (nextWaypointNumber == -1) {
nextWaypointNumber = 0; nextWaypointNumber = 0;
} }
@@ -136,7 +135,7 @@ public class MarkerEditActivity extends AbstractActivity {
waypointMarkerType.setText(""); waypointMarkerType.setText("");
waypointDescription.setText(""); waypointDescription.setText("");
} else { } else {
waypoint = ContentProviderUtils.Factory.get(this).getWaypoint(markerId); waypoint = new ContentProviderUtils(this).getWaypoint(markerId);
if (waypoint == null) { if (waypoint == null) {
Log.d(TAG, "waypoint is null"); Log.d(TAG, "waypoint is null");
finish(); finish();
@@ -182,6 +181,6 @@ public class MarkerEditActivity extends AbstractActivity {
waypoint.setCategory(waypointMarkerType.getText().toString()); waypoint.setCategory(waypointMarkerType.getText().toString());
waypoint.setDescription(waypointDescription.getText().toString()); waypoint.setDescription(waypointDescription.getText().toString());
} }
ContentProviderUtils.Factory.get(this).updateWaypoint(waypoint); new ContentProviderUtils(this).updateWaypoint(waypoint);
} }
} }
@@ -39,13 +39,13 @@ import androidx.loader.content.Loader;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointsColumns; import de.dennisguse.opentracks.content.WaypointsColumns;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment; import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller; import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.util.ActivityUtils; import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils; import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; 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 * Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected. * class. Anonymous inner class will get garbage collected.
*/ */
private final OnSharedPreferenceChangeListener private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
// Note that the key can be null // Note that the key can be null
@@ -130,7 +129,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this); recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = ContentProviderUtils.Factory.get(this); contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this); sharedPreferences = PreferencesUtils.getSharedPreferences(this);
long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L); long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
@@ -152,7 +151,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) { resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override @Override
public void bindView(View view, Context context, Cursor cursor) { public void bindView(View view, Context context, Cursor cursor) {
int typeIndex = cursor.getColumnIndex(WaypointsColumns.TYPE);
int nameIndex = cursor.getColumnIndex(WaypointsColumns.NAME); int nameIndex = cursor.getColumnIndex(WaypointsColumns.NAME);
int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME); int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
int categoryIndex = cursor.getColumnIndex(WaypointsColumns.CATEGORY); int categoryIndex = cursor.getColumnIndex(WaypointsColumns.CATEGORY);
@@ -161,13 +159,13 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
int latitudeIndex = cursor.getColumnIndex(WaypointsColumns.LATITUDE); int latitudeIndex = cursor.getColumnIndex(WaypointsColumns.LATITUDE);
int longitudeIndex = cursor.getColumnIndex(WaypointsColumns.LONGITUDE); int longitudeIndex = cursor.getColumnIndex(WaypointsColumns.LONGITUDE);
boolean statistics = WaypointType.values()[cursor.getInt(typeIndex)] == WaypointType.STATISTICS; int iconId = MarkerUtils.ICON_ID;
int iconId = statistics ? R.drawable.ic_marker_yellow_pushpin : R.drawable.ic_marker_blue_pushpin;
String name = cursor.getString(nameIndex); String name = cursor.getString(nameIndex);
long time = cursor.getLong(timeIndex); long time = cursor.getLong(timeIndex);
String category = statistics ? null : cursor.getString(categoryIndex); String category = cursor.getString(categoryIndex);
String description = statistics ? null : cursor.getString(descriptionIndex); String description = cursor.getString(descriptionIndex);
String photoUrl = cursor.getString(photoUrlIndex); String photoUrl = cursor.getString(photoUrlIndex);
//TODO also show latitude and longitude in list
double latitude = cursor.getDouble(latitudeIndex); double latitude = cursor.getDouble(latitudeIndex);
double longitude = cursor.getDouble(longitudeIndex); double longitude = cursor.getDouble(longitudeIndex);
@@ -246,7 +244,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (track != null && item.getItemId() == R.id.marker_list_insert_marker) { if (track != null && item.getItemId() == R.id.marker_list_insert_marker) {
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class) Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, track.getId()); .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.SearchEngine.SearchQuery;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint; import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment; import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment; import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller; 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.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils; import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -170,7 +170,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this); recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = ContentProviderUtils.Factory.get(this); contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this); sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null); trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
searchEngine = new SearchEngine(contentProviderUtils); searchEngine = new SearchEngine(contentProviderUtils);
@@ -393,11 +393,9 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
} }
} }
boolean statistics = waypoint.getType() == WaypointType.STATISTICS;
resultMap.put(IS_RECORDING_FIELD, false); resultMap.put(IS_RECORDING_FIELD, false);
resultMap.put(IS_PAUSED_FIELD, true); 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(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_marker);
resultMap.put(NAME_FIELD, waypoint.getName()); resultMap.put(NAME_FIELD, waypoint.getName());
// Display the marker's track name in the total time field // 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(TOTAL_DISTANCE_FIELD, null);
resultMap.put(MARKER_COUNT_FIELD, 0); resultMap.put(MARKER_COUNT_FIELD, 0);
resultMap.put(START_TIME_FIELD, waypoint.getLocation().getTime()); resultMap.put(START_TIME_FIELD, waypoint.getLocation().getTime());
resultMap.put(CATEGORY_FIELD, statistics ? null : waypoint.getCategory()); resultMap.put(CATEGORY_FIELD, waypoint.getCategory());
resultMap.put(DESCRIPTION_FIELD, statistics ? null : waypoint.getDescription()); resultMap.put(DESCRIPTION_FIELD, waypoint.getDescription());
resultMap.put(PHOTO_URL_FIELD, waypoint.getPhotoUrl()); resultMap.put(PHOTO_URL_FIELD, waypoint.getPhotoUrl());
resultMap.put(TRACK_ID_FIELD, waypoint.getTrackId()); resultMap.put(TRACK_ID_FIELD, waypoint.getTrackId());
resultMap.put(MARKER_ID_FIELD, waypoint.getId()); 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.FileUtils;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StatsUtils;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils; 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 // Note that sharedPreferenceChangeListener cannot be an anonymous inner class. Anonymous inner class will get garbage collected.
* class. Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.recording_track_id_key, key)) { if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(TrackDetailActivity.this); recordingTrackId = PreferencesUtils.getRecordingTrackId(TrackDetailActivity.this);
setLockscreenPolicy();
setScreenOnPolicy();
} }
if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.recording_track_paused_key, key)) { if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(TrackDetailActivity.this); recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(TrackDetailActivity.this);
setLockscreenPolicy();
setScreenOnPolicy();
} }
if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.stats_show_on_lockscreen_while_recording_key, key)) { if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.stats_show_on_lockscreen_while_recording_key, key)) {
setLockscreenPolicy(); setLockscreenPolicy();
} }
if (PreferencesUtils.isKey(TrackDetailActivity.this, R.string.stats_keep_screen_on_while_recording_key, key)) {
setScreenOnPolicy();
}
if (key == null) return; if (key == null) return;
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@@ -189,7 +194,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
photoUri = savedInstanceState != null ? (Uri) savedInstanceState.getParcelable(PHOTO_URI_KEY) : null; photoUri = savedInstanceState != null ? (Uri) savedInstanceState.getParcelable(PHOTO_URI_KEY) : null;
hasPhoto = savedInstanceState != null && savedInstanceState.getBoolean(HAS_PHOTO_KEY, false); hasPhoto = savedInstanceState != null && savedInstanceState.getBoolean(HAS_PHOTO_KEY, false);
contentProviderUtils = ContentProviderUtils.Factory.get(this); contentProviderUtils = new ContentProviderUtils(this);
handleIntent(getIntent()); handleIntent(getIntent());
sharedPreferences = PreferencesUtils.getSharedPreferences(this); sharedPreferences = PreferencesUtils.getSharedPreferences(this);
@@ -227,11 +232,13 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
@Override @Override
public void onAttachedToWindow() { public void onAttachedToWindow() {
setLockscreenPolicy(); setLockscreenPolicy();
setScreenOnPolicy();
super.onAttachedToWindow(); super.onAttachedToWindow();
} }
private void setLockscreenPolicy() { 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(showOnLockScreen); 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 @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
@@ -413,22 +431,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA
return trackDataHub; 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) { private void handleIntent(Intent intent) {
trackId = intent.getLongExtra(EXTRA_TRACK_ID, -1L); trackId = intent.getLongExtra(EXTRA_TRACK_ID, -1L);
long markerId = intent.getLongExtra(EXTRA_MARKER_ID, -1L); long markerId = intent.getLongExtra(EXTRA_MARKER_ID, -1L);
@@ -73,7 +73,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
return; return;
} }
contentProviderUtils = ContentProviderUtils.Factory.get(this); contentProviderUtils = new ContentProviderUtils(this);
track = contentProviderUtils.getTrack(trackId); track = contentProviderUtils.getTrack(trackId);
if (track == null) { if (track == null) {
Log.e(TAG, "No track for " + trackId); Log.e(TAG, "No track for " + trackId);
@@ -164,7 +164,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Menu items // Menu items
private MenuItem searchMenuItem; private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem; private MenuItem startGpsMenuItem;
private MenuItem deleteAllMenuItem; // private MenuItem deleteAllMenuItem;
private final OnClickListener stopListener = new OnClickListener() { private final OnClickListener stopListener = new OnClickListener() {
@Override @Override
@@ -245,7 +245,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this); recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = ContentProviderUtils.Factory.get(this); contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this); sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback); trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, bindChangedCallback);
@@ -371,7 +371,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
ActivityUtils.configureSearchWidget(this, searchMenuItem, trackController); ActivityUtils.configureSearchWidget(this, searchMenuItem, trackController);
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps); startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
deleteAllMenuItem = menu.findItem(R.id.track_list_delete_all);
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@@ -425,9 +424,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
intent = IntentUtils.newIntent(this, MarkerListActivity.class); intent = IntentUtils.newIntent(this, MarkerListActivity.class);
startActivity(intent); startActivity(intent);
return true; return true;
case R.id.track_list_delete_all:
deleteTracks(new long[]{-1L});
return true;
case R.id.track_list_settings: case R.id.track_list_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class); intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent); 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); startGpsMenuItem.setIcon(isGpsStarted ? R.drawable.ic_gps_fixed_24dp : R.drawable.ic_gps_off_24dp);
} }
} }
if (deleteAllMenuItem != null) {
deleteAllMenuItem.setVisible(hasTrack && !isRecording);
}
} }
/** /**
@@ -14,7 +14,7 @@
* the License. * the License.
*/ */
package de.dennisguse.opentracks; package de.dennisguse.opentracks.chart;
import android.content.Context; import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
@@ -23,8 +23,6 @@ import android.graphics.Paint.Align;
import android.graphics.Paint.Style; import android.graphics.Paint.Style;
import android.graphics.Path; import android.graphics.Path;
import androidx.annotation.VisibleForTesting;
import java.text.NumberFormat; import java.text.NumberFormat;
import de.dennisguse.opentracks.stats.ExtremityMonitor; import de.dennisguse.opentracks.stats.ExtremityMonitor;
@@ -203,7 +201,6 @@ class ChartValueSeries {
/** /**
* Gets the interval value. * Gets the interval value.
*/ */
@VisibleForTesting
int getInterval() { int getInterval() {
return interval; return interval;
} }
@@ -218,7 +215,6 @@ class ChartValueSeries {
/** /**
* Gets the max marker value. * Gets the max marker value.
*/ */
@VisibleForTesting
int getMaxMarkerValue() { int getMaxMarkerValue() {
return maxMarkerValue; return maxMarkerValue;
} }
@@ -14,7 +14,7 @@
* the License. * the License.
*/ */
package de.dennisguse.opentracks; package de.dennisguse.opentracks.chart;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -27,33 +27,40 @@ import android.graphics.Paint.Style;
import android.graphics.Path; import android.graphics.Path;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.view.GestureDetector;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.VelocityTracker; import android.view.ScaleGestureDetector;
import android.view.View; import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Scroller; import android.widget.Scroller;
import androidx.core.view.GestureDetectorCompat;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.util.ArrayList; 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.content.Waypoint;
import de.dennisguse.opentracks.stats.ExtremityMonitor; import de.dennisguse.opentracks.stats.ExtremityMonitor;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
/** /**
* Visualization of the chart. * Visualization of the chart.
* Provides support for zooming (via pinch), scrolling, flinging, and selecting shown markers (single touch).
* *
* @author Sandor Dornbush * @author Sandor Dornbush
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
public class ChartView extends View { public class ChartView extends View {
public static final float MEDIUM_TEXT_SIZE = 18f; static final float MEDIUM_TEXT_SIZE = 18f;
public static final float SMALL_TEXT_SIZE = 12f; 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 NUM_SERIES = 6;
public static final int ELEVATION_SERIES = 0; public static final int ELEVATION_SERIES = 0;
@@ -74,25 +81,28 @@ public class ChartView extends View {
private static final int SPACER = 4; private static final int SPACER = 4;
private static final int Y_AXIS_OFFSET = 16; 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 { static {
X_FRACTION_FORMAT.setMaximumFractionDigits(1); X_FRACTION_FORMAT.setMaximumFractionDigits(1);
X_FRACTION_FORMAT.setMinimumFractionDigits(1); X_FRACTION_FORMAT.setMinimumFractionDigits(1);
} }
private final ChartValueSeries[] series = new ChartValueSeries[NUM_SERIES]; private final ChartValueSeries[] series = new ChartValueSeries[NUM_SERIES];
private final ArrayList<double[]> chartData = new ArrayList<>(); private final List<double[]> chartData = new ArrayList<>();
private final ArrayList<Waypoint> waypoints = new ArrayList<>(); private final List<Waypoint> waypoints = new ArrayList<>();
private final ExtremityMonitor xExtremityMonitor = new ExtremityMonitor(); private final ExtremityMonitor xExtremityMonitor = new ExtremityMonitor();
private final Paint axisPaint; private final Paint axisPaint;
private final Paint xAxisMarkerPaint; private final Paint xAxisMarkerPaint;
private final Paint gridPaint; private final Paint gridPaint;
private final Paint markerPaint;
private final Drawable pointer; private final Drawable pointer;
private final Drawable waypointMarker;
private final int markerWidth; private final int markerWidth;
private final int markerHeight; private final int markerHeight;
private final Scroller scroller; private final Scroller scroller;
private double maxX = 1.0; private double maxX = 1.0;
private VelocityTracker velocityTracker = null;
private float lastMotionEventX = -1;
private int zoomLevel = 1; private int zoomLevel = 1;
private int leftBorder = BORDER; private int leftBorder = BORDER;
@@ -112,11 +122,76 @@ public class ChartView extends View {
private boolean reportSpeed = true; private boolean reportSpeed = true;
private boolean showPointer = false; private boolean showPointer = false;
/** private GestureDetectorCompat detectorScrollFlingTab = new GestureDetectorCompat(getContext(), new GestureDetector.SimpleOnGestureListener() {
* Constructor.
* @Override
* @param context the context public boolean onDown(MotionEvent e) {
*/ if (!scroller.isFinished()) {
scroller.abortAnimation();
}
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (Math.abs(distanceX) > 0) {
int availableToScroll = effectiveWidth * (zoomLevel - 1) - getScrollX();
if (availableToScroll > 0) {
scrollBy(Math.min(availableToScroll, (int) distanceX));
}
}
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
fling((int) -velocityX);
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent event) {
// Check if the y event is within markerHeight of the marker center
if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2f) < markerHeight) {
int minDistance = Integer.MAX_VALUE;
Waypoint nearestWaypoint = null;
synchronized (waypoints) {
for (Waypoint waypoint : waypoints) {
int distance = Math.abs(getX(getWaypointXValue(waypoint)) - (int) event.getX() - getScrollX());
if (distance < minDistance) {
minDistance = distance;
nearestWaypoint = waypoint;
}
}
}
if (nearestWaypoint != null && minDistance < markerWidth) {
Intent intent = IntentUtils.newIntent(getContext(), MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, nearestWaypoint.getId());
getContext().startActivity(intent);
return true;
}
}
return false;
}
});
private ScaleGestureDetector detectorZoom = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
if (scaleFactor >= 1.1f) {
zoomIn();
return true;
} else if (scaleFactor <= 0.9) {
zoomOut();
return true;
}
return false;
}
});
public ChartView(Context context, boolean chartByDistance) { public ChartView(Context context, boolean chartByDistance) {
super(context); super(context);
this.chartByDistance = chartByDistance; this.chartByDistance = chartByDistance;
@@ -187,7 +262,7 @@ public class ChartView extends View {
gridPaint.setAntiAlias(false); gridPaint.setAntiAlias(false);
gridPaint.setPathEffect(new DashPathEffect(new float[]{3, 2}, 0)); gridPaint.setPathEffect(new DashPathEffect(new float[]{3, 2}, 0));
Paint markerPaint = new Paint(); markerPaint = new Paint();
markerPaint.setStyle(Style.STROKE); markerPaint.setStyle(Style.STROKE);
markerPaint.setColor(context.getResources().getColor(android.R.color.darker_gray)); markerPaint.setColor(context.getResources().getColor(android.R.color.darker_gray));
markerPaint.setAntiAlias(false); markerPaint.setAntiAlias(false);
@@ -195,12 +270,9 @@ public class ChartView extends View {
pointer = context.getResources().getDrawable(R.drawable.ic_logo_color_24dp); pointer = context.getResources().getDrawable(R.drawable.ic_logo_color_24dp);
pointer.setBounds(0, 0, pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight()); pointer.setBounds(0, 0, pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight());
Drawable statisticsMarker = getResources().getDrawable(R.drawable.ic_marker_yellow_pushpin); waypointMarker = MarkerUtils.getDefaultPhoto(context);
markerWidth = statisticsMarker.getIntrinsicWidth(); markerWidth = waypointMarker.getIntrinsicWidth();
markerHeight = statisticsMarker.getIntrinsicHeight(); markerHeight = waypointMarker.getIntrinsicHeight();
statisticsMarker.setBounds(0, 0, markerWidth, markerHeight);
Drawable waypointMarker = getResources().getDrawable(R.drawable.ic_marker_blue_pushpin);
waypointMarker.setBounds(0, 0, markerWidth, markerHeight); waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
scroller = new Scroller(context); scroller = new Scroller(context);
@@ -223,16 +295,6 @@ public class ChartView extends View {
series[index].setEnabled(enabled); series[index].setEnabled(enabled);
} }
/**
* Sets chart by distance.
* It is expected that after changing this value, data will be reloaded.
*
* @param value true for by distance, false for by time
*/
public void setChartByDistance(boolean value) {
chartByDistance = value;
}
/** /**
* Sets metric units. * Sets metric units.
* *
@@ -265,11 +327,10 @@ public class ChartView extends View {
* *
* @param dataPoints an array of data points to be added * @param dataPoints an array of data points to be added
*/ */
public void addDataPoints(ArrayList<double[]> dataPoints) { public void addDataPoints(List<double[]> dataPoints) {
synchronized (chartData) { synchronized (chartData) {
chartData.addAll(dataPoints); chartData.addAll(dataPoints);
for (int i = 0; i < dataPoints.size(); i++) { for (double[] dataPoint : dataPoints) {
double[] dataPoint = dataPoints.get(i);
xExtremityMonitor.update(dataPoint[0]); xExtremityMonitor.update(dataPoint[0]);
for (int j = 0; j < series.length; j++) { for (int j = 0; j < series.length; j++) {
if (!Double.isNaN(dataPoint[j + 1])) { if (!Double.isNaN(dataPoint[j + 1])) {
@@ -411,79 +472,14 @@ public class ChartView extends View {
@Override @Override
public boolean onTouchEvent(MotionEvent event) { public boolean onTouchEvent(MotionEvent event) {
if (velocityTracker == null) { boolean isZoom = detectorZoom.onTouchEvent(event);
velocityTracker = VelocityTracker.obtain(); boolean isScrollTab = detectorScrollFlingTab.onTouchEvent(event);
} return isZoom || isScrollTab;
velocityTracker.addMovement(event);
float x = event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Stop the fling
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
lastMotionEventX = x;
break;
case MotionEvent.ACTION_MOVE:
if (lastMotionEventX == -1) {
break;
}
// Scroll to follow the motion event
int deltaX = (int) (lastMotionEventX - x);
lastMotionEventX = x;
if (deltaX < 0) {
if (getScrollX() > 0) {
scrollBy(deltaX);
}
} else if (deltaX > 0) {
int availableToScroll = effectiveWidth * (zoomLevel - 1) - getScrollX();
if (availableToScroll > 0) {
scrollBy(Math.min(availableToScroll, deltaX));
}
}
break;
case MotionEvent.ACTION_UP:
// Check if the y event is within markerHeight of the marker center
if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2) < markerHeight) {
int minDistance = Integer.MAX_VALUE;
Waypoint nearestWaypoint = null;
synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) {
Waypoint waypoint = waypoints.get(i);
int distance = Math.abs(getX(getWaypointXValue(waypoint)) - (int) event.getX() - getScrollX());
if (distance < minDistance) {
minDistance = distance;
nearestWaypoint = waypoint;
}
}
}
if (nearestWaypoint != null && minDistance < markerWidth) {
Intent intent = IntentUtils.newIntent(getContext(), MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, nearestWaypoint.getId());
getContext().startActivity(intent);
return true;
}
}
VelocityTracker myVelocityTracker = velocityTracker;
myVelocityTracker.computeCurrentVelocity(1000);
int initialVelocity = (int) myVelocityTracker.getXVelocity();
if (Math.abs(initialVelocity) > ViewConfiguration.getMinimumFlingVelocity()) {
fling(-initialVelocity);
}
if (velocityTracker != null) {
velocityTracker.recycle();
velocityTracker = null;
}
break;
}
return true;
} }
@Override @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
updateEffectiveDimensionsIfChanged( updateEffectiveDimensionsIfChanged(View.MeasureSpec.getSize(widthMeasureSpec), View.MeasureSpec.getSize(heightMeasureSpec));
View.MeasureSpec.getSize(widthMeasureSpec), View.MeasureSpec.getSize(heightMeasureSpec));
super.onMeasure(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec);
} }
@@ -499,6 +495,7 @@ public class ChartView extends View {
clipToGraphArea(canvas); clipToGraphArea(canvas);
drawDataSeries(canvas); drawDataSeries(canvas);
drawWaypoints(canvas);
drawGrid(canvas); drawGrid(canvas);
canvas.restore(); canvas.restore();
@@ -539,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. * Draws the grid.
* *
@@ -546,9 +570,9 @@ public class ChartView extends View {
*/ */
private void drawGrid(Canvas canvas) { private void drawGrid(Canvas canvas) {
// X axis grid // X axis grid
ArrayList<Double> xAxisMarkerPositions = getXAxisMarkerPositions(getXAxisInterval()); List<Double> xAxisMarkerPositions = getXAxisMarkerPositions(getXAxisInterval());
for (int i = 0; i < xAxisMarkerPositions.size(); i++) { for (double position : xAxisMarkerPositions) {
int x = getX(xAxisMarkerPositions.get(i)); int x = getX(position);
canvas.drawLine(x, topBorder, x, topBorder + effectiveHeight, gridPaint); canvas.drawLine(x, topBorder, x, topBorder + effectiveHeight, gridPaint);
} }
// Y axis grid // Y axis grid
@@ -620,10 +644,10 @@ public class ChartView extends View {
canvas.drawText(label, x + effectiveWidth + spacer, y + yOffset, axisPaint); canvas.drawText(label, x + effectiveWidth + spacer, y + yOffset, axisPaint);
double interval = getXAxisInterval(); double interval = getXAxisInterval();
ArrayList<Double> markerPositions = getXAxisMarkerPositions(interval);
NumberFormat numberFormat = interval < 1 ? X_FRACTION_FORMAT : X_NUMBER_FORMAT; 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);
} }
} }
@@ -633,8 +657,7 @@ public class ChartView extends View {
private String getXAxisLabel() { private String getXAxisLabel() {
Context context = getContext(); Context context = getContext();
if (chartByDistance) { if (chartByDistance) {
return metricUnits ? context.getString(R.string.unit_kilometer) return metricUnits ? context.getString(R.string.unit_kilometer) : context.getString(R.string.unit_mile);
: context.getString(R.string.unit_mile);
} else { } else {
return context.getString(R.string.description_time); return context.getString(R.string.description_time);
} }
@@ -674,8 +697,8 @@ public class ChartView extends View {
/** /**
* Gets the x axis marker positions. * Gets the x axis marker positions.
*/ */
private ArrayList<Double> getXAxisMarkerPositions(double interval) { private List<Double> getXAxisMarkerPositions(double interval) {
ArrayList<Double> markers = new ArrayList<>(); List<Double> markers = new ArrayList<>();
markers.add(0d); markers.add(0d);
for (int i = 1; i * interval < maxX; i++) { for (int i = 1; i * interval < maxX; i++) {
markers.add(i * interval); markers.add(i * interval);
@@ -734,8 +757,7 @@ public class ChartView extends View {
* @param yValue the y value * @param yValue the y value
* @return the marker width. * @return the marker width.
*/ */
private float drawYAxisMarker( private float drawYAxisMarker(ChartValueSeries chartValueSeries, Canvas canvas, int xPosition, int yValue) {
ChartValueSeries chartValueSeries, Canvas canvas, int xPosition, int yValue) {
String marker = chartValueSeries.formatMarker(yValue); String marker = chartValueSeries.formatMarker(yValue);
Paint paint = chartValueSeries.getMarkerPaint(); Paint paint = chartValueSeries.getMarkerPaint();
Rect rect = getRect(paint, marker); Rect rect = getRect(paint, marker);
@@ -786,8 +808,7 @@ public class ChartView extends View {
private void drawPaths() { private void drawPaths() {
boolean[] hasMoved = new boolean[series.length]; boolean[] hasMoved = new boolean[series.length];
for (int i = 0; i < chartData.size(); i++) { for (double[] dataPoint : chartData) {
double[] dataPoint = chartData.get(i);
for (int j = 0; j < series.length; j++) { for (int j = 0; j < series.length; j++) {
double value = dataPoint[j + 1]; double value = dataPoint[j + 1];
if (Double.isNaN(value)) { if (Double.isNaN(value)) {
@@ -870,8 +891,7 @@ public class ChartView extends View {
topBorder = (int) (density * BORDER + titleDimensions[0] * (titleDimensions[1] + spacer)); topBorder = (int) (density * BORDER + titleDimensions[0] * (titleDimensions[1] + spacer));
Rect xAxisLabelRect = getRect(axisPaint, getXAxisLabel()); Rect xAxisLabelRect = getRect(axisPaint, getXAxisLabel());
// border + x axis marker + spacer + .5 x axis label // border + x axis marker + spacer + .5 x axis label
bottomBorder = (int) (density * BORDER + getRect(xAxisMarkerPaint, "1").height() + spacer bottomBorder = (int) (density * BORDER + getRect(xAxisMarkerPaint, "1").height() + spacer + (xAxisLabelRect.height() / 2));
+ (xAxisLabelRect.height() / 2));
rightBorder = (int) (density * BORDER + xAxisLabelRect.width() + spacer); rightBorder = (int) (density * BORDER + xAxisLabelRect.width() + spacer);
updateEffectiveDimensions(); updateEffectiveDimensions();
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
package de.dennisguse.opentracks.content;
import android.location.Location;
import android.location.LocationManager;
/**
* Creates a new {@link SensorDataSetLocation}.
* An implementation can create new instances or reuse existing instances for optimization.
*/
public class LocationFactory {
/**
* The default {@link LocationFactory} which creates a location each time.
*/
public static LocationFactory DEFAULT_LOCATION_FACTORY = new LocationFactory();
public Location createLocation() {
return new SensorDataSetLocation(LocationManager.GPS_PROVIDER);
}
}
@@ -0,0 +1,22 @@
package de.dennisguse.opentracks.content;
import android.database.Cursor;
import android.location.Location;
import java.util.Iterator;
/**
* A lightweight wrapper around the original {@link Cursor} with a method to clean up.
*/
public interface LocationIterator extends Iterator<Location>, AutoCloseable {
/**
* Gets the most recently retrieved track point id by {@link #next()}.
*/
long getLocationId();
/**
* Closes the iterator.
*/
void close();
}
@@ -198,7 +198,7 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
@Nullable @Nullable
@Override @Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
ContentProviderUtils contentProviderUtils = new ContentProviderUtilsImpl(this); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
long[] trackIds = parseURI(uri); long[] trackIds = parseURI(uri);
final Track[] tracks = new Track[trackIds.length]; final Track[] tracks = new Track[trackIds.length];
@@ -30,7 +30,6 @@ import java.util.EnumSet;
import java.util.Set; import java.util.Set;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.util.LocationUtils; import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -41,17 +40,24 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
*/ */
public class TrackDataHub implements DataSourceListener { 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. * Target number of track points displayed by the map overlay.
* We may display more than this number of points. * We may display more than this number of points.
*/ */
static final int TARGET_DISPLAYED_TRACK_POINTS = 5000; @Deprecated
private static final int TARGET_DISPLAYED_TRACK_POINTS = 5000;
/** /**
* Maximum number of waypoints to displayed. * Maximum number of waypoints to displayed.
*/ */
@VisibleForTesting @VisibleForTesting
@Deprecated
private static final int MAX_DISPLAYED_WAYPOINTS = 128; private static final int MAX_DISPLAYED_WAYPOINTS = 128;
private static final String TAG = TrackDataHub.class.getSimpleName(); private static final String TAG = TrackDataHub.class.getSimpleName();
private final Context context; private final Context context;
@@ -96,11 +102,6 @@ public class TrackDataHub implements DataSourceListener {
resetSamplingState(); resetSamplingState();
} }
public synchronized static TrackDataHub newInstance(Context context) {
return new TrackDataHub(context, new TrackDataManager(), ContentProviderUtils.Factory.get(
context), TARGET_DISPLAYED_TRACK_POINTS);
}
public void start() { public void start() {
if (started) { if (started) {
Log.i(TAG, "TrackDataHub already started, ignoring start."); Log.i(TAG, "TrackDataHub already started, ignoring start.");
@@ -398,8 +399,7 @@ public class TrackDataHub implements DataSourceListener {
trackDataListener.clearWaypoints(); trackDataListener.clearWaypoints();
} }
try (Cursor cursor = contentProviderUtils.getWaypointCursor( try (Cursor cursor = contentProviderUtils.getWaypointCursor(selectedTrackId, -1L, MAX_DISPLAYED_WAYPOINTS)) {
selectedTrackId, -1L, MAX_DISPLAYED_WAYPOINTS)) {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
do { do {
Waypoint waypoint = contentProviderUtils.createWaypoint(cursor); Waypoint waypoint = contentProviderUtils.createWaypoint(cursor);
@@ -448,7 +448,7 @@ public class TrackDataHub implements DataSourceListener {
int samplingFrequency = -1; int samplingFrequency = -1;
boolean includeNextPoint = false; boolean includeNextPoint = false;
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) { try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(selectedTrackId, localLastSeenLocationId + 1, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
while (locationIterator.hasNext()) { while (locationIterator.hasNext()) {
Location location = locationIterator.next(); Location location = locationIterator.next();
@@ -468,12 +468,8 @@ public class TrackDataHub implements DataSourceListener {
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints); samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
} }
if (!LocationUtils.isValidLocation(location)) { if (!LocationUtils.isValidLocation(location)) { //This can be split markers (not anymore supported feature)
// TODO: also include the last valid point before a split includeNextPoint = true;
for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSegmentSplit(location);
includeNextPoint = true;
}
} else { } else {
// Also include the last point if the selected track is not recording. // Also include the last point if the selected track is not recording.
if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0) || (locationId == lastTrackPointId && !isSelectedTrackRecording())) { if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0) || (locationId == lastTrackPointId && !isSelectedTrackRecording())) {
@@ -51,14 +51,9 @@ public interface TrackDataListener {
*/ */
void onSampledOutTrackPoint(Location location); 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. * 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(); void onNewTrackPointsDone();
@@ -188,6 +188,7 @@ public final class Waypoint {
} }
public enum WaypointType { 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_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); public final static WaypointCreationRequest DEFAULT_START_TRACK = new WaypointCreationRequest(WaypointType.STATISTICS, true);
private WaypointType type; private WaypointType type;
@@ -5,8 +5,8 @@ public final class SensorDataSet {
public static final float DATA_UNAVAILABLE = Float.NaN; public static final float DATA_UNAVAILABLE = Float.NaN;
//TODO It might be necessary to consider: if sensor was connected as well. //TODO It might be necessary to consider: if sensor was connected as well.
public String sensorName; private String sensorName;
public String sensorAddress; private String sensorAddress;
private float heartRate; private float heartRate;
private float cadence; private float cadence;
private float power; private float power;
@@ -22,7 +22,6 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.LayoutParams;
import android.widget.ZoomControls;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -32,10 +31,11 @@ import androidx.fragment.app.FragmentActivity;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List;
import de.dennisguse.opentracks.ChartView;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity; import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.SensorDataSetLocation; import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.TrackDataHub; import de.dennisguse.opentracks.content.TrackDataHub;
@@ -59,7 +59,7 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
private static final String STATE_CHART_VIEW_BY_DISTANCE_KEY = "chartViewByDistance"; 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; private TrackDataHub trackDataHub;
@@ -77,10 +77,9 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
// UI elements // UI elements
private ChartView chartView; private ChartView chartView;
private ZoomControls zoomControls;
/** /**
* A runnable that will enable/disable zoom controls and orange pointer as appropriate and redraw. * A runnable that will set the orange pointer as appropriate and redraw.
*/ */
private final Runnable updateChart = new Runnable() { private final Runnable updateChart = new Runnable() {
@Override @Override
@@ -89,8 +88,6 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
return; return;
} }
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
chartView.setShowPointer(isSelectedTrackRecording()); chartView.setShowPointer(isSelectedTrackRecording());
chartView.invalidate(); chartView.invalidate();
} }
@@ -107,26 +104,12 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
recordingDistanceInterval = PreferencesUtils.getRecordingDistanceIntervalDefault(getContext()); 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. // 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 @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.chart, container, false); return inflater.inflate(R.layout.chart, container, false);
zoomControls = view.findViewById(R.id.chart_zoom_controls);
zoomControls.setOnZoomInClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
zoomIn();
}
});
zoomControls.setOnZoomOutClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
zoomOut();
}
});
return view;
} }
@Override @Override
@@ -216,13 +199,6 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
} }
} }
@Override
public void onSegmentSplit(Location location) {
if (isResumed()) {
fillDataPoint(location, null);
}
}
@Override @Override
public void onNewTrackPointsDone() { public void onNewTrackPointsDone() {
if (isResumed()) { if (isResumed()) {
@@ -282,9 +258,8 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
} }
reportSpeed = speed; reportSpeed = speed;
chartView.setReportSpeed(reportSpeed); chartView.setReportSpeed(reportSpeed);
boolean chartShowSpeed = PreferencesUtils.shouldChartShowSpeed(getActivity()); setSeriesEnabled(ChartView.SPEED_SERIES, reportSpeed);
setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed); setSeriesEnabled(ChartView.PACE_SERIES, !reportSpeed);
setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed);
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -321,26 +296,14 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
*/ */
private void checkChartSettings() { private void checkChartSettings() {
boolean needUpdate = false; 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; 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) { if (needUpdate) {
chartView.postInvalidate(); chartView.postInvalidate();
} }
@@ -401,18 +364,6 @@ public abstract class ChartFragment extends Fragment implements TrackDataListene
} }
} }
private void zoomIn() {
chartView.zoomIn();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
private void zoomOut() {
chartView.zoomOut();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
/** /**
* Runs a runnable on the UI thread if possible. * Runs a runnable on the UI thread if possible.
* *
@@ -81,7 +81,7 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(fragmentActivity); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(fragmentActivity);
for (long markerId : markerIds) { for (long markerId : markerIds) {
contentProviderUtils.deleteWaypoint(fragmentActivity, contentProviderUtils.deleteWaypoint(fragmentActivity,
markerId, new DescriptionGeneratorImpl(fragmentActivity)); markerId, new DescriptionGeneratorImpl(fragmentActivity));
@@ -41,12 +41,10 @@ import androidx.fragment.app.FragmentActivity;
import de.dennisguse.opentracks.MarkerEditActivity; import de.dennisguse.opentracks.MarkerEditActivity;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint; import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils; 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.StringUtils;
import de.dennisguse.opentracks.util.UnitConversions; 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 static final long HIDE_TEXT_DELAY = 4 * UnitConversions.ONE_SECOND;
private ContentProviderUtils contentProviderUtils; private ContentProviderUtils contentProviderUtils;
private Handler handler; private Handler handler;
private ImageView photo; private ImageView photoView;
private ImageView textGradient; private ImageView textGradient;
private LinearLayout waypointInfo; private LinearLayout waypointInfo;
private Waypoint waypoint; private Waypoint waypoint;
@@ -110,7 +108,7 @@ public class MarkerDetailFragment extends Fragment {
getFragmentManager().popBackStack(); getFragmentManager().popBackStack();
return; return;
} }
contentProviderUtils = ContentProviderUtils.Factory.get(getActivity()); contentProviderUtils = new ContentProviderUtils(getActivity());
handler = new Handler(); handler = new Handler();
setHasOptionsMenu(true); setHasOptionsMenu(true);
} }
@@ -119,11 +117,11 @@ public class MarkerDetailFragment extends Fragment {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.marker_detail_fragment, container, false); 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); textGradient = view.findViewById(R.id.marker_detail_waypoint_text_gradient);
waypointInfo = view.findViewById(R.id.marker_detail_waypoint_info); waypointInfo = view.findViewById(R.id.marker_detail_waypoint_info);
photo.setOnClickListener(new View.OnClickListener() { photoView.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
handler.removeCallbacks(hideText); handler.removeCallbacks(hideText);
@@ -156,11 +154,8 @@ public class MarkerDetailFragment extends Fragment {
@Override @Override
public void setMenuVisibility(boolean menuVisible) { public void setMenuVisibility(boolean menuVisible) {
super.setMenuVisibility(menuVisible); super.setMenuVisibility(menuVisible);
/* // View pager caches the neighboring fragments in the resumed state.
* View pager caches the neighboring fragments in the resumed state. If // If becoming visible from the resumed state, update the UI to display the text above the image.
* becoming visible from the resumed state, update the UI to display the
* text above the image.
*/
if (isResumed()) { if (isResumed()) {
if (menuVisible) { if (menuVisible) {
updateUi(); updateUi();
@@ -214,47 +209,25 @@ public class MarkerDetailFragment extends Fragment {
} }
} }
/**
* Updates the UI.
*/
private void updateUi() { private void updateUi() {
View waypointView = getView().findViewById(R.id.marker_detail_waypoint); boolean hasPhoto = waypoint.hasPhoto();
View statisticsView = getView().findViewById(R.id.marker_detail_statistics); if (hasPhoto) {
handler.removeCallbacks(hideText);
boolean isWaypoint = waypoint.getType() == WaypointType.WAYPOINT; photoView.setImageURI(waypoint.getPhotoURI());
waypointView.setVisibility(isWaypoint ? View.VISIBLE : View.GONE); handler.postDelayed(hideText, HIDE_TEXT_DELAY);
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);
} else { } else {
setName(R.id.marker_detail_statistics_name, false); photoView.setImageResource(MarkerUtils.ICON_ID);
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);
} }
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) { private void setName(int resId, boolean addShadow) {
@@ -20,16 +20,18 @@ import android.location.Location;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import java.util.EnumSet; 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.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.ITrackRecordingService; import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.stats.TripStatistics; import de.dennisguse.opentracks.stats.TripStatistics;
import de.dennisguse.opentracks.util.LocationUtils; 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.TrackIconUtils;
import de.dennisguse.opentracks.util.UnitConversions; 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(); public static final String STATS_FRAGMENT_TAG = StatsFragment.class.getSimpleName();
private static final long UI_UPDATE_INTERVAL = UnitConversions.ONE_SECOND;
private TrackDataHub trackDataHub; private TrackDataHub trackDataHub;
private Handler handlerUpdateUI; private Handler handlerUpdateUI;
//TODO Initialize immediately and remove in onDestroy()
private TrackRecordingServiceConnection trackRecordingServiceConnection; private TrackRecordingServiceConnection trackRecordingServiceConnection;
private Location lastLocation = null; private Location lastLocation = null;
private TripStatistics lastTripStatistics = 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 String category = "";
private int recordingGpsAccuracy; private int recordingGpsAccuracy;
@@ -87,6 +82,111 @@ public class StatsFragment extends Fragment implements TrackDataListener {
return inflater.inflate(R.layout.stats, container, false); 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 @Override
public void onActivityCreated(Bundle savedInstanceState) { public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
@@ -125,10 +225,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null); trackRecordingServiceConnection = new TrackRecordingServiceConnection(getContext(), null);
trackRecordingServiceConnection.startConnection(getContext()); trackRecordingServiceConnection.startConnection(getContext());
updateUi(getActivity()); handlerUpdateUI.post(updateUIeachSecond);
if (isSelectedTrackRecording()) {
handlerUpdateUI.post(updateUIeachSecond);
}
} }
@Override @Override
@@ -141,10 +238,66 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override @Override
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
trackRecordingServiceConnection.unbind(); if (trackRecordingServiceConnection != null) {
trackRecordingServiceConnection.unbind();
}
trackRecordingServiceConnection = null; 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 @Override
public void onTrackUpdated(final Track track) { public void onTrackUpdated(final Track track) {
if (isResumed()) { if (isResumed()) {
@@ -154,7 +307,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
if (isResumed()) { if (isResumed()) {
lastTripStatistics = track != null ? track.getTripStatistics() : null; lastTripStatistics = track != null ? track.getTripStatistics() : null;
category = track != null ? track.getCategory() : ""; category = track != null ? track.getCategory() : "";
updateUi(getActivity()); updateUI();
} }
} }
}); });
@@ -176,11 +329,6 @@ public class StatsFragment extends Fragment implements TrackDataListener {
lastLocation = location; lastLocation = location;
} }
@Override
public void onSegmentSplit(Location location) {
// We don't care.
}
@Override @Override
public void onNewTrackPointsDone() { public void onNewTrackPointsDone() {
if (isResumed()) { if (isResumed()) {
@@ -200,7 +348,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
lastLocation = null; lastLocation = null;
} }
} }
StatsUtils.setLocationValues(getActivity(), lastLocation, isSelectedTrackRecording()); setLocationValues();
} }
} }
}); });
@@ -229,7 +377,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override @Override
public void run() { public void run() {
if (isResumed()) { if (isResumed()) {
updateUi(getActivity()); updateUI();
} }
} }
}); });
@@ -244,7 +392,7 @@ public class StatsFragment extends Fragment implements TrackDataListener {
@Override @Override
public void run() { public void run() {
if (isResumed()) { if (isResumed()) {
updateUi(getActivity()); updateUI();
} }
} }
}); });
@@ -300,6 +448,12 @@ public class StatsFragment extends Fragment implements TrackDataListener {
return trackDataHub != null && trackDataHub.isSelectedTrackPaused(); 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}. * 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(); sensorDataSet = trackRecordingService.getSensorData();
} }
StatsUtils.setSensorData(getActivity(), sensorDataSet, isSelectedTrackRecording()); setHeartRateSensorData(sensorDataSet, isSelectedTrackRecording());
} }
/** private void setHeartRateSensorData(SensorDataSet sensorDataSet, boolean isRecording) {
* Updates the UI. // heart rate
*/ int isVisible = View.VISIBLE;
private void updateUi(FragmentActivity activity) { if (!isRecording || PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(getContext())) {
String trackIconValue = TrackIconUtils.getIconValue(activity, category); isVisible = View.INVISIBLE;
StatsUtils.setTripStatisticsValues(activity, lastTripStatistics, trackIconValue); }
StatsUtils.setLocationValues(activity, lastLocation, isSelectedTrackRecording()); sensorContainerView.setVisibility(isVisible);
updateSensorDataUI();
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) { 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() { public String getExtension() {
@@ -173,10 +173,12 @@ public enum TrackFileFormat {
private static final String MIME_KML = "application/vnd.google-earth.kml+xml"; private static final String MIME_KML = "application/vnd.google-earth.kml+xml";
public TrackExporter newTrackExporter(Context context, Track[] tracks, TrackExporterListener trackExporterListener) { private static TrackExporter newKmzTrackExporter(Context context, TrackWriter trackWriter, Track[] tracks, TrackExporterListener trackExporterListener, boolean exportPhotos) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
TrackWriter trackWriter = this.newTrackWriter(context, tracks.length > 1);
return new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener); FileTrackExporter fileTrackExporter = new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener);
return new KmzTrackExporter(contentProviderUtils, fileTrackExporter, tracks, exportPhotos);
} }
/** /**
@@ -184,12 +186,10 @@ public enum TrackFileFormat {
*/ */
public abstract String getMimeType(); public abstract String getMimeType();
private static TrackExporter newKmzTrackExporter(Context context, TrackWriter trackWriter, Track[] tracks, TrackExporterListener trackExporterListener, boolean exportPhotos) { public TrackExporter newTrackExporter(Context context, Track[] tracks, TrackExporterListener trackExporterListener) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
TrackWriter trackWriter = newTrackWriter(context, tracks.length > 1);
FileTrackExporter fileTrackExporter = new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener); return new FileTrackExporter(contentProviderUtils, trackWriter, tracks, trackExporterListener);
return new KmzTrackExporter(contentProviderUtils, fileTrackExporter, tracks, exportPhotos);
} }
/** /**
@@ -103,16 +103,8 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
} }
directoryDisplayName = FileUtils.getPathDisplayName(trackFileFormat.getExtension()); 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 = new ExportAsyncTask(this, trackFileFormat, directory);
exportAsyncTask.execute(); exportAsyncTask.execute();
// }
} }
@Override @Override
@@ -68,7 +68,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
this.trackFileFormat = trackFileFormat; this.trackFileFormat = trackFileFormat;
this.directory = directory; this.directory = directory;
context = exportActivity.getApplicationContext(); context = exportActivity.getApplicationContext();
contentProviderUtils = ContentProviderUtils.Factory.get(context); contentProviderUtils = new ContentProviderUtils(context);
completed = false; completed = false;
processedTrackCount = 0; processedTrackCount = 0;
@@ -26,7 +26,8 @@ import androidx.annotation.NonNull;
import java.io.OutputStream; import java.io.OutputStream;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator; import de.dennisguse.opentracks.content.LocationFactory;
import de.dennisguse.opentracks.content.LocationIterator;
import de.dennisguse.opentracks.content.SensorDataSetLocation; import de.dennisguse.opentracks.content.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint; import de.dennisguse.opentracks.content.Waypoint;
@@ -63,7 +64,7 @@ public class FileTrackExporter implements TrackExporter {
} }
@Override @Override
public boolean writeTrack(Context context, @NonNull OutputStream outputStream) { public boolean writeTrack(@NonNull Context context, @NonNull OutputStream outputStream) {
try { try {
trackWriter.prepare(outputStream); trackWriter.prepare(outputStream);
trackWriter.writeHeader(tracks); trackWriter.writeHeader(tracks);
@@ -209,7 +210,7 @@ public class FileTrackExporter implements TrackExporter {
* *
* @author Jimmy Shih * @author Jimmy Shih
*/ */
private class TrackWriterLocationFactory implements ContentProviderUtils.LocationFactory { private class TrackWriterLocationFactory extends LocationFactory {
Location currentLocation; Location currentLocation;
Location lastLocation; Location lastLocation;
@@ -89,7 +89,7 @@ public class KmlTrackWriter implements TrackWriter {
this.exportSensorData = exportSensorData; this.exportSensorData = exportSensorData;
this.exportPhotos = exportPhotos; this.exportPhotos = exportPhotos;
this.descriptionGenerator = new DescriptionGeneratorImpl(context); this.descriptionGenerator = new DescriptionGeneratorImpl(context);
this.contentProviderUtils = ContentProviderUtils.Factory.get(context); this.contentProviderUtils = new ContentProviderUtils(context);
} }
@Override @Override
@@ -19,7 +19,6 @@ package de.dennisguse.opentracks.io.file.exporter;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.OutputStream; import java.io.OutputStream;
@@ -37,5 +36,5 @@ public interface TrackExporter {
* @param context the context * @param context the context
* @param outputStream the output stream * @param outputStream the output stream
*/ */
boolean writeTrack(@Nullable Context context, @NonNull OutputStream outputStream); boolean writeTrack(@NonNull Context context, @NonNull OutputStream outputStream);
} }
@@ -39,8 +39,9 @@ import javax.xml.parsers.SAXParserFactory;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.content.DescriptionGeneratorImpl; import de.dennisguse.opentracks.content.DescriptionGeneratorImpl;
import de.dennisguse.opentracks.content.LocationFactory;
import de.dennisguse.opentracks.content.LocationIterator;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint; import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType; import de.dennisguse.opentracks.content.Waypoint.WaypointType;
@@ -165,7 +166,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime()); TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime()); TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(track.getTripStatistics().getStartTime());
try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) { try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
while (true) { while (true) {
if (waypoint == null) { if (waypoint == null) {
@@ -55,7 +55,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
* @param context the context * @param context the context
*/ */
public GpxFileTrackImporter(Context context) { public GpxFileTrackImporter(Context context) {
this(context, ContentProviderUtils.Factory.get(context)); this(context, new ContentProviderUtils(context));
} }
@VisibleForTesting @VisibleForTesting
@@ -45,7 +45,9 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ImportAsyncTask.class.getSimpleName(); private static final String TAG = ImportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat; private final TrackFileFormat trackFileFormat;
private final String path; private final String path;
private final Context context; // TODO Can be removed, or?
private Context context;
// TODO Use weak reference
private ImportActivity importActivity; private ImportActivity importActivity;
private WakeLock wakeLock; private WakeLock wakeLock;
@@ -100,7 +102,7 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override @Override
protected Boolean doInBackground(Void... params) { protected Boolean doInBackground(Void... params) {
try { try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); Thread.currentThread().setPriority(Thread.MAX_PRIORITY); // TODO Should be set to previous level at the end of this method.
// Get the wake lock if not recording or paused // Get the wake lock if not recording or paused
boolean isRecording = PreferencesUtils.isRecording(importActivity); boolean isRecording = PreferencesUtils.isRecording(importActivity);
boolean isPaused = PreferencesUtils.isRecordingTrackPaused(importActivity); boolean isPaused = PreferencesUtils.isRecordingTrackPaused(importActivity);
@@ -149,9 +151,11 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
@Override @Override
protected void onCancelled() { protected void onCancelled() {
context = null;
completed = true; completed = true;
if (importActivity != null) { if (importActivity != null) {
importActivity.onAsyncTaskCompleted(importTrackCount, totalTrackCount); importActivity.onAsyncTaskCompleted(importTrackCount, totalTrackCount);
importActivity = null;
} }
} }
@@ -163,17 +167,17 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private boolean importFile(final File file) { private boolean importFile(final File file) {
TrackImporter trackImporter; TrackImporter trackImporter;
if (trackFileFormat == TrackFileFormat.GPX) { if (trackFileFormat == TrackFileFormat.GPX) {
trackImporter = new GpxFileTrackImporter(context); trackImporter = new GpxFileTrackImporter(importActivity);
} else { //KML or KMZ } else { //KML or KMZ
String extension = FileUtils.getExtension(file.getName()); String extension = FileUtils.getExtension(file.getName());
if (TrackFileFormat.KML_ONLY_TRACK.getExtension().equals(extension)) { if (TrackFileFormat.KML_ONLY_TRACK.getExtension().equals(extension)) {
trackImporter = new KmlFileTrackImporter(context, -1L); trackImporter = new KmlFileTrackImporter(importActivity, -1L);
} else { } else {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(importActivity);
Uri uri = contentProviderUtils.insertTrack(new Track()); Uri uri = contentProviderUtils.insertTrack(new Track());
long newId = Long.parseLong(uri.getLastPathSegment()); long newId = Long.parseLong(uri.getLastPathSegment());
trackImporter = new KmzTrackImporter(context, newId); trackImporter = new KmzTrackImporter(importActivity, newId);
} }
} }
@@ -81,7 +81,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
* @param importTrackId track id to import to. -1L to import to a new track. * @param importTrackId track id to import to. -1L to import to a new track.
*/ */
public KmlFileTrackImporter(Context context, long importTrackId) { public KmlFileTrackImporter(Context context, long importTrackId) {
this(context, importTrackId, ContentProviderUtils.Factory.get(context)); this(context, importTrackId, new ContentProviderUtils(context));
} }
@VisibleForTesting @VisibleForTesting
@@ -72,7 +72,7 @@ public class KmzTrackImporter implements TrackImporter {
return -1L; return -1L;
} }
String fileName = zipEntry.getName(); String fileName = zipEntry.getName();
if (fileName.equals(KmzTrackExporter.KMZ_KML_FILE)) { if (KmzTrackExporter.KMZ_KML_FILE.equals(fileName)) {
trackId = parseKml(zipInputStream); trackId = parseKml(zipInputStream);
if (trackId == -1L) { if (trackId == -1L) {
Log.d(TAG, "Unable to parse kml in kmz"); Log.d(TAG, "Unable to parse kml in kmz");
@@ -102,7 +102,7 @@ public class KmzTrackImporter implements TrackImporter {
*/ */
private void cleanImport(long trackId) { private void cleanImport(long trackId) {
if (PreferencesUtils.isRecording(trackId)) { if (PreferencesUtils.isRecording(trackId)) {
ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(context); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
contentProviderUtils.deleteTrack(context, trackId); contentProviderUtils.deleteTrack(context, trackId);
} }
@@ -125,15 +125,10 @@ public class KmzTrackImporter implements TrackImporter {
* @return the imported track id or -1L * @return the imported track id or -1L
*/ */
private long parseKml(ZipInputStream zipInputStream) throws IOException { private long parseKml(ZipInputStream zipInputStream) throws IOException {
ByteArrayInputStream byteArrayInputStream = null; KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, importTrackId);
try {
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, importTrackId); try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream))) {
byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream));
return kmlFileTrackImporter.importFile(byteArrayInputStream); return kmlFileTrackImporter.importFile(byteArrayInputStream);
} finally {
if (byteArrayInputStream != null) {
byteArrayInputStream.close();
}
} }
} }
@@ -160,12 +155,10 @@ public class KmzTrackImporter implements TrackImporter {
* @param fileName the file name * @param fileName the file name
*/ */
private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException { private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException {
if (importTrackId == -1L) { if (importTrackId == -1L || fileName.equals("")) {
return;
}
if (fileName.equals("")) {
return; return;
} }
File dir = FileUtils.getPhotoDir(importTrackId); File dir = FileUtils.getPhotoDir(importTrackId);
FileUtils.ensureDirectoryExists(dir); FileUtils.ensureDirectoryExists(dir);
File file = new File(dir, fileName); File file = new File(dir, fileName);
@@ -43,19 +43,17 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackDetailActivity; import de.dennisguse.opentracks.TrackDetailActivity;
import de.dennisguse.opentracks.TrackListActivity; import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.content.ContentProviderUtils; import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.ContentProviderUtils.LocationIterator;
import de.dennisguse.opentracks.content.CustomContentProvider; 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.SensorDataSetLocation;
import de.dennisguse.opentracks.content.Track; import de.dennisguse.opentracks.content.Track;
import de.dennisguse.opentracks.content.Waypoint; import de.dennisguse.opentracks.content.Waypoint;
import de.dennisguse.opentracks.content.Waypoint.WaypointType;
import de.dennisguse.opentracks.content.WaypointCreationRequest; import de.dennisguse.opentracks.content.WaypointCreationRequest;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager; import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory; import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor; 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.TripStatistics;
import de.dennisguse.opentracks.stats.TripStatisticsUpdater; import de.dennisguse.opentracks.stats.TripStatisticsUpdater;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
@@ -74,8 +72,6 @@ import de.dennisguse.opentracks.util.UnitConversions;
*/ */
public class TrackRecordingService extends Service { 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; private static final int NOTIFICATION_ID = 123;
public static final double PAUSE_LATITUDE = 100.0; public static final double PAUSE_LATITUDE = 100.0;
@@ -83,8 +79,6 @@ public class TrackRecordingService extends Service {
// Anything faster than that (in meters per second) will be considered moving. // Anything faster than that (in meters per second) will be considered moving.
public static final double MAX_NO_MOVEMENT_SPEED = 0.224; 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(); private static final String TAG = TrackRecordingService.class.getSimpleName();
// 1 minute in milliseconds // 1 minute in milliseconds
private static final long ONE_MINUTE = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS); private static final long ONE_MINUTE = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS);
@@ -94,7 +88,6 @@ public class TrackRecordingService extends Service {
private Handler handler; private Handler handler;
private LocationManagerConnector locationManagerConnector; private LocationManagerConnector locationManagerConnector;
private PeriodicTaskExecutor voiceExecutor; private PeriodicTaskExecutor voiceExecutor;
private PeriodicTaskExecutor splitExecutor;
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private TrackRecordingServiceNotificationManager notificationManager; private TrackRecordingServiceNotificationManager notificationManager;
private long recordingTrackId; private long recordingTrackId;
@@ -103,7 +96,6 @@ public class TrackRecordingService extends Service {
private int recordingDistanceInterval; private int recordingDistanceInterval;
private int maxRecordingDistance; private int maxRecordingDistance;
private int recordingGpsAccuracy; private int recordingGpsAccuracy;
private int autoResumeTrackTimeout;
private long currentRecordingInterval; private long currentRecordingInterval;
// The following variables are set when recording: // The following variables are set when recording:
@@ -125,14 +117,10 @@ public class TrackRecordingService extends Service {
if (PreferencesUtils.isKey(context, R.string.stats_units_key, key)) { if (PreferencesUtils.isKey(context, R.string.stats_units_key, key)) {
boolean metricUnits = PreferencesUtils.isMetricUnits(context); boolean metricUnits = PreferencesUtils.isMetricUnits(context);
voiceExecutor.setMetricUnits(metricUnits); voiceExecutor.setMetricUnits(metricUnits);
splitExecutor.setMetricUnits(metricUnits);
} }
if (PreferencesUtils.isKey(context, R.string.voice_frequency_key, key)) { if (PreferencesUtils.isKey(context, R.string.voice_frequency_key, key)) {
voiceExecutor.setTaskFrequency(PreferencesUtils.getVoiceFrequency(context)); 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)) { if (PreferencesUtils.isKey(context, R.string.min_recording_interval_key, key)) {
int minRecordingInterval = PreferencesUtils.getMinRecordingInterval(context); int minRecordingInterval = PreferencesUtils.getMinRecordingInterval(context);
if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptBatteryLife(context)) { if (minRecordingInterval == PreferencesUtils.getMinRecordingIntervalAdaptBatteryLife(context)) {
@@ -154,12 +142,9 @@ public class TrackRecordingService extends Service {
if (PreferencesUtils.isKey(context, R.string.recording_gps_accuracy_key, key)) { if (PreferencesUtils.isKey(context, R.string.recording_gps_accuracy_key, key)) {
recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(context); 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 WakeLock wakeLock;
private BluetoothRemoteSensorManager remoteSensorManager; private BluetoothRemoteSensorManager remoteSensorManager;
private Location lastLocation; private Location lastLocation;
@@ -214,11 +199,10 @@ public class TrackRecordingService extends Service {
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
executorService = Executors.newSingleThreadExecutor(); executorService = Executors.newSingleThreadExecutor();
contentProviderUtils = ContentProviderUtils.Factory.get(this); contentProviderUtils = new ContentProviderUtils(this);
handler = new Handler(); handler = new Handler();
locationManagerConnector = new LocationManagerConnector(this, handler.getLooper()); locationManagerConnector = new LocationManagerConnector(this, handler.getLooper());
voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory()); voiceExecutor = new PeriodicTaskExecutor(this, new AnnouncementPeriodicTaskFactory());
splitExecutor = new PeriodicTaskExecutor(this, new SplitPeriodicTaskFactory());
sharedPreferences = PreferencesUtils.getSharedPreferences(this); sharedPreferences = PreferencesUtils.getSharedPreferences(this);
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
notificationManager = new TrackRecordingServiceNotificationManager(this); notificationManager = new TrackRecordingServiceNotificationManager(this);
@@ -246,7 +230,6 @@ public class TrackRecordingService extends Service {
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
handleStartCommand(intent, startId);
return START_STICKY; return START_STICKY;
} }
@@ -271,12 +254,6 @@ public class TrackRecordingService extends Service {
// unregister sharedPreferences before shutting down splitExecutor and voiceExecutor // unregister sharedPreferences before shutting down splitExecutor and voiceExecutor
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
try {
splitExecutor.shutdown();
} finally {
splitExecutor = null;
}
try { try {
voiceExecutor.shutdown(); voiceExecutor.shutdown();
} finally { } finally {
@@ -297,23 +274,14 @@ public class TrackRecordingService extends Service {
super.onDestroy(); super.onDestroy();
} }
/**
* Returns true if the service is recording.
*/
public boolean isRecording() { public boolean isRecording() {
return PreferencesUtils.isRecording(recordingTrackId); return PreferencesUtils.isRecording(recordingTrackId);
} }
/**
* Returns true if the current recording is paused.
*/
public boolean isPaused() { public boolean isPaused() {
return recordingTrackPaused; return recordingTrackPaused;
} }
/**
* Gets the trip statistics.
*/
public TripStatistics getTripStatistics() { public TripStatistics getTripStatistics() {
if (trackTripStatisticsUpdater == null) { if (trackTripStatisticsUpdater == null) {
return null; return null;
@@ -332,43 +300,19 @@ public class TrackRecordingService extends Service {
return -1L; return -1L;
} }
WaypointType waypointType = waypointCreationRequest.getType(); String name = waypointCreationRequest.getName();
boolean isStatistics = waypointType == WaypointType.STATISTICS; if (name == null) {
int nextWaypointNumber = contentProviderUtils.getNextWaypointNumber(recordingTrackId, waypointCreationRequest.getType());
// Get name
String name;
if (waypointCreationRequest.getName() != null) {
name = waypointCreationRequest.getName();
} else {
int nextWaypointNumber = contentProviderUtils.getNextWaypointNumber(recordingTrackId, waypointType);
if (nextWaypointNumber == -1) { if (nextWaypointNumber == -1) {
nextWaypointNumber = 0; 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 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; double length;
long duration; long duration;
Location location = getLastValidTrackPointInCurrentSegment(recordingTrackId); Location location = getLastValidTrackPointInCurrentSegment(recordingTrackId);
@@ -391,63 +335,11 @@ public class TrackRecordingService extends Service {
String photoUrl = waypointCreationRequest.getPhotoUrl() != null ? waypointCreationRequest.getPhotoUrl() : ""; String photoUrl = waypointCreationRequest.getPhotoUrl() != null ? waypointCreationRequest.getPhotoUrl() : "";
// Insert waypoint // 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); Uri uri = contentProviderUtils.insertWaypoint(waypoint);
return Long.parseLong(uri.getLastPathSegment()); 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. * Starts a new track.
* *
@@ -460,7 +352,6 @@ public class TrackRecordingService extends Service {
} }
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
trackTripStatisticsUpdater = new TripStatisticsUpdater(now); trackTripStatisticsUpdater = new TripStatisticsUpdater(now);
markerTripStatisticsUpdater = new TripStatisticsUpdater(now);
// Insert a track // Insert a track
Track track = new Track(); Track track = new Track();
@@ -469,7 +360,6 @@ public class TrackRecordingService extends Service {
// Update shared preferences // Update shared preferences
updateRecordingState(trackId, false); updateRecordingState(trackId, false);
PreferencesUtils.resetAutoResumeTrackCurrentRetryDefault(this);
// Update database // Update database
track.setId(trackId); track.setId(trackId);
@@ -480,6 +370,8 @@ public class TrackRecordingService extends Service {
track.setIcon(TrackIconUtils.getIconValue(this, category)); track.setIcon(TrackIconUtils.getIconValue(this, category));
track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics()); track.setTripStatistics(trackTripStatisticsUpdater.getTripStatistics());
contentProviderUtils.updateTrack(track); contentProviderUtils.updateTrack(track);
//TODO Do not use insertWaypoint
insertWaypoint(WaypointCreationRequest.DEFAULT_START_TRACK); insertWaypoint(WaypointCreationRequest.DEFAULT_START_TRACK);
startRecording(true); startRecording(true);
@@ -497,23 +389,11 @@ public class TrackRecordingService extends Service {
TripStatistics tripStatistics = track.getTripStatistics(); TripStatistics tripStatistics = track.getTripStatistics();
trackTripStatisticsUpdater = new TripStatisticsUpdater(tripStatistics.getStartTime()); trackTripStatisticsUpdater = new TripStatisticsUpdater(tripStatistics.getStartTime());
long markerStartTime; try (LocationIterator locationIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false, LocationFactory.DEFAULT_LOCATION_FACTORY)) {
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, ContentProviderUtils.DEFAULT_LOCATION_FACTORY)) {
while (locationIterator.hasNext()) { while (locationIterator.hasNext()) {
Location location = locationIterator.next(); Location location = locationIterator.next();
trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval); trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
if (location.getTime() > markerStartTime) {
markerTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
}
} }
} catch (RuntimeException e) { } catch (RuntimeException e) {
Log.e(TAG, "RuntimeException", e); Log.e(TAG, "RuntimeException", e);
@@ -548,8 +428,7 @@ public class TrackRecordingService extends Service {
} }
/** /**
* Common code for starting a new track, resuming a track, or restarting after * Common code for starting a new track, resuming a track, or restarting after phone reboot.
* phone reboot.
* *
* @param trackStarted true if track is started, false if track is resumed * @param trackStarted true if track is started, false if track is resumed
*/ */
@@ -565,7 +444,6 @@ public class TrackRecordingService extends Service {
// Restore periodic tasks // Restore periodic tasks
voiceExecutor.restore(); voiceExecutor.restore();
splitExecutor.restore();
} }
/** /**
@@ -610,7 +488,7 @@ public class TrackRecordingService extends Service {
contentProviderUtils.updateTrack(track); contentProviderUtils.updateTrack(track);
} }
} }
endRecording(true, trackId); endRecording(true);
} }
/** /**
@@ -638,7 +516,7 @@ public class TrackRecordingService extends Service {
insertLocation(track, pause, null); insertLocation(track, pause, null);
} }
endRecording(false, recordingTrackId); endRecording(false);
notificationManager.updateContent(getString(R.string.generic_paused)); notificationManager.updateContent(getString(R.string.generic_paused));
} }
@@ -647,12 +525,10 @@ public class TrackRecordingService extends Service {
* Common code for ending a track or pausing a track. * Common code for ending a track or pausing a track.
* *
* @param trackStopped true if track is stopped, false if track is paused * @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 // Shutdown periodic tasks
voiceExecutor.shutdown(); voiceExecutor.shutdown();
splitExecutor.shutdown();
// Update instance variables // Update instance variables
if (remoteSensorManager != null) { if (remoteSensorManager != null) {
@@ -829,7 +705,6 @@ public class TrackRecordingService extends Service {
Uri uri = contentProviderUtils.insertTrackPoint(location, track.getId()); Uri uri = contentProviderUtils.insertTrackPoint(location, track.getId());
long trackPointId = Long.parseLong(uri.getLastPathSegment()); long trackPointId = Long.parseLong(uri.getLastPathSegment());
trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval); trackTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
markerTripStatisticsUpdater.addLocation(location, recordingDistanceInterval);
updateRecordingTrack(track, trackPointId, LocationUtils.isValidLocation(location)); updateRecordingTrack(track, trackPointId, LocationUtils.isValidLocation(location));
} catch (SQLiteException e) { } catch (SQLiteException e) {
/* /*
@@ -839,7 +714,6 @@ public class TrackRecordingService extends Service {
Log.w(TAG, "SQLiteException", e); Log.w(TAG, "SQLiteException", e);
} }
voiceExecutor.update(); voiceExecutor.update();
splitExecutor.update();
} }
/** /**
@@ -1030,15 +904,7 @@ public class TrackRecordingService extends Service {
@Override @Override
public SensorDataSet getSensorData() { public SensorDataSet getSensorData() {
if (trackRecordingService.remoteSensorManager == null) { return trackRecordingService.getSensorDataSet();
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();
} }
/** /**
@@ -48,6 +48,8 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
private static final String TAG = TrackRecordingServiceConnection.class.getSimpleName(); 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 Context context;
private final Runnable callback; private final Runnable callback;
@@ -86,11 +86,11 @@ public class BluetoothRemoteSensorManager {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (bluetoothConnectionManager != null && PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_heart_rate_key, key)) { if (bluetoothConnectionManager != null && PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_heart_rate_key, key)) {
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context); if (PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(context)) {
if (address.equals(PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT)) {
stop(); stop();
return; return;
} }
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
if (bluetoothConnectionManager.isSameBluetoothDevice(address)) { if (bluetoothConnectionManager.isSameBluetoothDevice(address)) {
return; return;
} }
@@ -145,12 +145,11 @@ public class BluetoothRemoteSensorManager {
return; return;
} }
// String address = PreferencesUtils.getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT); if (PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(context)) {
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
if (PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(address)) {
Log.w(TAG, "No bluetooth address."); Log.w(TAG, "No bluetooth address.");
return; return;
} }
String address = PreferencesUtils.getBluetoothHeartRateSensorAddress(context);
Log.i(TAG, "Connecting to bluetooth address: " + address); Log.i(TAG, "Connecting to bluetooth address: " + address);
BluetoothDevice device; BluetoothDevice device;
@@ -21,12 +21,8 @@ import android.media.AudioManager;
import android.speech.tts.TextToSpeech; import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener; import android.speech.tts.TextToSpeech.OnInitListener;
import android.speech.tts.UtteranceProgressListener; import android.speech.tts.UtteranceProgressListener;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log; import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.util.Locale; import java.util.Locale;
import de.dennisguse.opentracks.R; 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.stats.TripStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TTSUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
/** /**
@@ -47,22 +42,54 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
/** /**
* The rate at which announcements are spoken. * The rate at which announcements are spoken.
*/ */
@VisibleForTesting private static final float TTS_SPEECH_RATE = 0.9f;
static final float TTS_SPEECH_RATE = 0.9f;
private static final String TAG = AnnouncementPeriodicTask.class.getSimpleName(); private static final String TAG = AnnouncementPeriodicTask.class.getSimpleName();
private final Context context; private final Context context;
private final AudioManager audioManager; 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() { private final UtteranceProgressListener utteranceListener = new UtteranceProgressListener() {
@Override @Override
public void onStart(String utteranceId) { 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 @Override
public void onDone(String utteranceId) { public void onDone(String utteranceId) {
int result = audioManager.abandonAudioFocus(null); int result = audioManager.abandonAudioFocus(audioFocusChangeListener);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "Failed to relinquish audio focus."); Log.w(TAG, "Failed to relinquish audio focus.");
} }
@@ -70,32 +97,16 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
@Override @Override
public void onError(String utteranceId) { public void onError(String utteranceId) {
Log.e(TAG, "An error occurred for utteranceId " + utteranceId);
} }
}; };
private TextToSpeech tts; private TextToSpeech tts;
// Response from TTS after its initialization // Response from TTS after its initialization
private int initStatus = TextToSpeech.ERROR; private int ttsInitStatus = TextToSpeech.ERROR;
private boolean ttsReady = false; 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) { AnnouncementPeriodicTask(Context context) {
this.context = context; this.context = context;
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@@ -103,16 +114,17 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
@Override @Override
public void start() { public void start() {
Log.d(TAG, "Start");
if (tts == null) { if (tts == null) {
tts = newTextToSpeech(context, new OnInitListener() { tts = new TextToSpeech(context, new OnInitListener() {
@Override @Override
public void onInit(int status) { 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 @Override
@@ -129,8 +141,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
* *
* @param tripStatistics the trip statistics * @param tripStatistics the trip statistics
*/ */
@VisibleForTesting private void announce(TripStatistics tripStatistics) {
void announce(TripStatistics tripStatistics) {
if (tripStatistics == null) { if (tripStatistics == null) {
Log.e(TAG, "TripStatistics is null."); Log.e(TAG, "TripStatistics is null.");
return; return;
@@ -138,7 +149,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
synchronized (this) { synchronized (this) {
if (!ttsReady) { if (!ttsReady) {
ttsReady = initStatus == TextToSpeech.SUCCESS; ttsReady = ttsInitStatus == TextToSpeech.SUCCESS;
if (ttsReady) { if (ttsReady) {
onTtsReady(); 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."); Log.i(TAG, "Speech is not allowed at this time.");
return; return;
} }
@@ -158,21 +169,18 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
@Override @Override
public void shutdown() { public void shutdown() {
listenToPhoneState(phoneStateListener, PhoneStateListener.LISTEN_NONE);
if (tts != null) { if (tts != null) {
tts.shutdown(); tts.shutdown();
tts = null; tts = null;
} }
} }
/**
* Called when TTS is ready.
*/
private void onTtsReady() { private void onTtsReady() {
Locale locale = Locale.getDefault(); Locale locale = Locale.getDefault();
int languageAvailability = tts.isLanguageAvailable(locale); int languageAvailability = tts.isLanguageAvailable(locale);
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA if (languageAvailability == TextToSpeech.LANG_MISSING_DATA || languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
|| languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
Log.w(TAG, "Default locale not available, use English."); Log.w(TAG, "Default locale not available, use English.");
locale = Locale.ENGLISH; locale = Locale.ENGLISH;
/* /*
@@ -188,42 +196,12 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
tts.setOnUtteranceProgressListener(utteranceListener); tts.setOnUtteranceProgressListener(utteranceListener);
} }
/**
* Speaks the announcement.
*
* @param announcement the announcement
*/
private void speakAnnouncement(String announcement) { private void speakAnnouncement(String announcement) {
int result = audioManager.requestAudioFocus(null, TTSUtils.getTTSStream(), AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); // We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
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.
*/
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used"); tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
} }
/** private String getAnnouncement(TripStatistics tripStatistics) {
* 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) {
boolean metricUnits = PreferencesUtils.isMetricUnits(context); boolean metricUnits = PreferencesUtils.isMetricUnits(context);
boolean reportSpeed = PreferencesUtils.isReportSpeed(context); boolean reportSpeed = PreferencesUtils.isReportSpeed(context);
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM; double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
@@ -256,27 +234,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
getAnnounceTime(tripStatistics.getMovingTime()), rate); getAnnounceTime(tripStatistics.getMovingTime()), rate);
} }
/** private String getAnnounceTime(long time) {
* 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) {
int[] parts = StringUtils.getTimeParts(time); int[] parts = StringUtils.getTimeParts(time);
String seconds = context.getResources() String seconds = context.getResources()
.getQuantityString(R.plurals.voiceSeconds, parts[0], parts[0]); .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()); LayoutInflater inflater = LayoutInflater.from(parent.getContext());
//TODO Check if there is a better way to achieve identical look and feel to ListPreference. //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 //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); Device device = devices.get(position);
@@ -24,6 +24,7 @@ import java.util.List;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.BluetoothUtils; import de.dennisguse.opentracks.util.BluetoothUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/** /**
* Preference to select a discoverable Bluetooth LE device. * 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 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) { public BluetoothLePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes); super(context, attrs, defStyleAttr, defStyleRes);
} }
@@ -75,6 +78,10 @@ public class BluetoothLePreference extends DialogPreference {
@Override @Override
public CharSequence getSummary() { public CharSequence getSummary() {
if (getValue() == null || PreferencesUtils.isBluetoothHeartRateSensorAddressDefault(getContext(), getValue())) {
return getContext().getString(DEVICE_NONE_RESOURCEID);
}
return getValue(); return getValue();
} }
@@ -142,12 +149,12 @@ public class BluetoothLePreference extends DialogPreference {
} }
String deviceNone = getContext().getString(R.string.sensor_type_value_none); 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; selectedEntryIndex = 0;
BluetoothLePreference preference = (BluetoothLePreference) getPreference(); BluetoothLePreference preference = (BluetoothLePreference) getPreference();
String deviceSelected = preference.getValue(); String deviceSelected = preference.getValue();
if (!deviceNone.equals(deviceSelected)) { if (deviceSelected != null && !deviceNone.equals(deviceSelected)) {
listAdapter.add(preference.getValue(), preference.getValue()); listAdapter.add(preference.getValue(), preference.getValue());
selectedEntryIndex = 1; selectedEntryIndex = 1;
} }
@@ -142,22 +142,4 @@ final class PreferenceHelper {
return entries; 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() { public void onResume() {
super.onResume(); super.onResume();
PreferencesUtils.getSharedPreferences(getActivity()).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); PreferencesUtils.getSharedPreferences(getActivity()).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
updateSpeed();
updateUnits(); updateUnits();
updateReset(); updateReset();
updateBluetooth(); updateBluetooth();
@@ -150,11 +141,6 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
resetPreference.setEnabled(!isRecording); 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() { private void updateBluetooth() {
// Disable Bluetooth preference if device does not have Bluetooth // Disable Bluetooth preference if device does not have Bluetooth
BluetoothLePreference bluetoothPreference = findPreference(getString(R.string.settings_sensor_bluetooth_heart_rate_key)); 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)); ListPreference voiceFrequency = findPreference(getString(R.string.voice_frequency_key));
voiceFrequency.setEntries(StringUtils.getFrequencyOptions(getActivity(), metricUnits)); 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)); ListPreference minRecordingInterval = findPreference(getString(R.string.min_recording_interval_key));
minRecordingInterval.setEntries(PreferenceHelper.getMinRecordingIntervalEntries(getActivity())); 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)); ListPreference recordingGpsAccuracy = findPreference(getString(R.string.recording_gps_accuracy_key));
recordingGpsAccuracy.setEntries(PreferenceHelper.getRecordingGpsAccuracyEntries(getActivity(), metricUnits)); 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)); 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); String[] entries = getResources().getStringArray(metricUnits ? R.array.stats_rate_metric_options : R.array.stats_rate_imperial_options);
statsRatePreferences.setEntries(entries); statsRatePreferences.setEntries(entries);
@@ -204,12 +204,13 @@ public class FileUtils {
* *
* @param name name * @param name name
*/ */
// TODO Check if this function is still needed.
public static String sanitizeFileName(String name) { public static String sanitizeFileName(String name) {
StringBuilder builder = new StringBuilder(name.length()); StringBuilder builder = new StringBuilder(name.length());
for (int i = 0; i < name.length(); i++) { for (int i = 0; i < name.length(); i++) {
int codePoint = name.codePointAt(i); int codePoint = name.codePointAt(i);
char character = name.charAt(i); char character = name.charAt(i);
if (Character.isLetterOrDigit(character) || codePoint > 127 || isSpecialFat32(character)) { if (Character.isLetterOrDigit(character) || codePoint > 127 || isSpecialFat32(character) || character == '.') {
builder.appendCodePoint(codePoint); builder.appendCodePoint(codePoint);
} else { } else {
builder.append("_"); builder.append("_");
@@ -76,7 +76,7 @@ public class IntentUtils {
} }
String trackDescription = ""; String trackDescription = "";
if (trackIds.length == 1) { if (trackIds.length == 1) {
Track track = ContentProviderUtils.Factory.get(context).getTrack(trackIds[0]); Track track = new ContentProviderUtils(context).getTrack(trackIds[0]);
trackDescription = track == null ? "" : new DescriptionGeneratorImpl(context).generateTrackDescription(track, false); trackDescription = track == null ? "" : new DescriptionGeneratorImpl(context).generateTrackDescription(track, false);
} }
@@ -89,13 +89,13 @@ public class IntentUtils {
uris.add(uriAndMime.first); uris.add(uriAndMime.first);
mime = uriAndMime.second; mime = uriAndMime.second;
} }
return new Intent(action) return new Intent(action)
.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.share_track_subject))
.putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_track_share_file_body, trackDescription))
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
.setType(mime) .setType(mime)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); .setAction(Intent.ACTION_SEND_MULTIPLE)
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.share_track_subject))
.putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_track_share_file_body, trackDescription));
} }
public static void showCoordinateOnMap(Context context, Waypoint waypoint) { public static void showCoordinateOnMap(Context context, Waypoint waypoint) {
@@ -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;
import android.content.SharedPreferences.Editor; import android.content.SharedPreferences.Editor;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
@@ -32,8 +31,6 @@ import de.dennisguse.opentracks.R;
*/ */
public class PreferencesUtils { public class PreferencesUtils {
public static final String BLUETOOTH_SENSOR_DEFAULT = "";
private PreferencesUtils() { private PreferencesUtils() {
} }
@@ -43,29 +40,13 @@ public class PreferencesUtils {
@Deprecated @Deprecated
//NOTE: is at the moment still used to determine if a track is currently recorded; better ask the service directly. //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 final long RECORDING_TRACK_ID_DEFAULT = -1L;
public static long getRecordingTrackId(Context context) { public static long getRecordingTrackId(Context context) {
return PreferencesUtils.getLong(context, R.string.recording_track_id_key, RECORDING_TRACK_ID_DEFAULT); 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) { public static String getDefaultActivity(Context context) {
return PreferencesUtils.getString(context, R.string.default_activity_key, context.getString(R.string.default_activity_default)); 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. * Compares if keyId and key belong to the same shared preference key.
* *
* @param keyId The resource id of the 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 * @return true if key == null or key belongs to keyId
*/ */
public static boolean isKey(Context context, int keyId, String key) { public static boolean isKey(Context context, int keyId, String key) {
@@ -211,33 +192,16 @@ public class PreferencesUtils {
editor.apply(); editor.apply();
} }
/**
* Returns true if metric units.
*
* @param context the context
*/
public static boolean isMetricUnits(Context context) { public static boolean isMetricUnits(Context context) {
final String STATS_UNIT = context.getString(R.string.stats_units_default); final String STATS_UNIT = context.getString(R.string.stats_units_default);
return STATS_UNIT.equals(getString(context, R.string.stats_units_key, STATS_UNIT)); 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) { public static boolean isReportSpeed(Context context) {
final String STATS_RATE_DEFAULT = context.getString(R.string.stats_rate_default); 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)); 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) { public static boolean isRecordingTrackPaused(Context context) {
return PreferencesUtils.getBoolean(context, R.string.recording_track_paused_key, isRecordingTrackPausedDefault(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); 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) { 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) { public static String getBluetoothHeartRateSensorAddress(Context context) {
return PreferencesUtils.getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT); return PreferencesUtils.getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothHeartRateSensorAddressNone(context));
}
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);
} }
public static boolean shouldShowStatsOnLockscreen(Context 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); 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) { public static boolean isShowStatsGradeElevation(Context context) {
final boolean STATS_SHOW_GRADE_ELEVATION = context.getResources().getBoolean(R.bool.stats_show_grade_elevation_default); 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); 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); 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) { 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)); 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) { public static int getRecordingGPSAccuracy(Context context) {
final int RECORDING_GPS_ACCURACY = Integer.parseInt(context.getResources().getString(R.string.recording_gps_accuracy_default)); 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); 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). * 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) { if (decimalPlaces < 1) {
return Long.toString(Math.round(value)); return Long.toString(Math.round(value));
} }
@@ -158,7 +158,7 @@ public class StringUtils {
* @param metricUnits true to use metric unit * @param metricUnits true to use metric unit
* @return the formatted distance (or null) and it's unit as {@link Pair} * @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)) { if (Double.isNaN(distance) || Double.isInfinite(distance)) {
return new Pair<>(null, context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet)); 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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:baselineAligned="false" android:baselineAligned="false"
android:orientation="horizontal"> android:orientation="vertical">
<include layout="@layout/toolbar" /> <include layout="@layout/toolbar" />
<include layout="@layout/track_controller" /> <LinearLayout
android:layout_width="match_parent"
<ListView
android:id="@+id/track_list"
android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1.0" /> android:baselineAligned="false"
android:orientation="horizontal">
<RelativeLayout <include layout="@layout/track_controller" />
android:id="@+id/track_list_empty_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0">
<LinearLayout <ListView
android:id="@+id/track_list_arrow_long" android:id="@+id/track_list"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_alignParentLeft="true" android:layout_weight="1.0" />
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingRight="16dp">
<View <RelativeLayout
android:layout_width="match_parent" android:id="@+id/track_list_empty_view"
android:layout_height="0dp" android:layout_width="0dp"
android:layout_weight="1" /> 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_width="wrap_content"
android:layout_height="0dp" android:layout_height="match_parent"
android:layout_weight="10" android:layout_alignParentLeft="true"
android:contentDescription="@string/image_arrow" android:orientation="vertical"
android:src="@drawable/ic_arrow_long" /> android:paddingLeft="12dp"
android:paddingRight="16dp">
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="17" /> android:layout_weight="1" />
</LinearLayout>
<LinearLayout <ImageView
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_toRightOf="@id/track_list_arrow_long" android:layout_weight="10"
android:orientation="vertical"> android:contentDescription="@string/image_arrow"
android:src="@drawable/ic_arrow_long" />
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="4" /> android:layout_weight="17" />
</LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
android:layout_weight="24"> android:layout_toRightOf="@id/track_list_arrow_long"
android:orientation="vertical">
<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 <View
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_weight=".3" /> 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>
</LinearLayout> </RelativeLayout>
</RelativeLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
+4 -18
View File
@@ -14,22 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/chart_view_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
android:orientation="vertical"> android:layout_weight="1"
android:orientation="vertical" />
<LinearLayout
android:id="@+id/chart_view_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical" />
<ZoomControls
android:id="@+id/chart_zoom_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:gravity="right"
android:paddingRight="8dp" />
</LinearLayout>
@@ -15,7 +15,8 @@ limitations under the License.
--> -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar" /> <include layout="@layout/toolbar" />
+23 -58
View File
@@ -17,85 +17,50 @@ limitations under the License.
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<FrameLayout <ImageView
android:id="@+id/marker_detail_waypoint" android:id="@+id/marker_detail_waypoint_photo"
android:layout_width="match_parent" 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 <ImageView
android:id="@+id/marker_detail_waypoint_photo" android:id="@+id/marker_detail_waypoint_text_gradient"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@android:color/black" android:alpha=".6"
android:contentDescription="@string/image_photo_marker" android:contentDescription="@string/image_photo_marker"
android:scaleType="fitCenter" /> android:src="@drawable/text_gradient" />
<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">
<LinearLayout <LinearLayout
android:id="@+id/marker_detail_waypoint_info"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="80dp"
android:orientation="vertical" android:orientation="vertical"
android:padding="8dp"> android:padding="8dp">
<TextView <TextView
android:id="@+id/marker_detail_statistics_name" android:id="@+id/marker_detail_waypoint_name"
style="@style/TextMedium" style="@style/TextMedium"
android:paddingTop="8dp" /> android:paddingTop="8dp" />
<TextView <TextView
android:id="@+id/marker_detail_statistics_location" android:id="@+id/marker_detail_waypoint_category"
style="@style/TextSmall" /> style="@style/TextSmall" />
<TextView <TextView
style="@style/TextSmall" android:id="@+id/marker_detail_waypoint_description"
android:paddingTop="4dp" style="@style/TextSmall" />
android:paddingBottom="6dp"
android:text="@string/marker_detail_statistics_title" />
<include layout="@layout/stats_content" /> <TextView
android:id="@+id/marker_detail_waypoint_location"
style="@style/TextSmall" />
</LinearLayout> </LinearLayout>
</ScrollView> </FrameLayout>
</FrameLayout> </FrameLayout>
+1 -1
View File
@@ -41,7 +41,7 @@ limitations under the License.
<EditText <EditText
android:id="@+id/marker_edit_statistics_name" android:id="@+id/marker_edit_statistics_name"
style="@style/EditNameMarker" style="@style/EditNameMarker"
android:drawableLeft="@drawable/ic_marker_yellow_pushpin" android:drawableLeft="@drawable/ic_marker_blue_pushpin"
android:hint="@string/generic_name" android:hint="@string/generic_name"
android:imeOptions="actionDone" android:imeOptions="actionDone"
android:importantForAutofill="no" /> android:importantForAutofill="no" />
+381 -1
View File
@@ -21,6 +21,386 @@ limitations under the License.
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> 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> </LinearLayout>
</ScrollView> </ScrollView>

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