Compare commits

..

46 Commits

Author SHA1 Message Date
Dennis Guse 2cdf122e8c Release: v3.18.1 2021-05-19 18:16:45 +02:00
mondstern 495ef2fc6f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/uk/
2021-05-19 17:15:57 +02:00
FTno ff561c1b25 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-05-19 17:15:57 +02:00
mondstern 7106f64a3c Translated using Weblate (Hungarian)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/hu/
2021-05-19 17:15:57 +02:00
mondstern 8420f9febb Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2021-05-19 17:15:57 +02:00
Xosé M 0744ba790f Translated using Weblate (Galician)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2021-05-19 17:15:57 +02:00
Dennis Guse 9a7f24bddc Update README. 2021-05-19 17:15:51 +02:00
dependabot[bot] ef47c86e7c Bump appcompat from 1.2.0 to 1.3.0
Bumps appcompat from 1.2.0 to 1.3.0.

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-19 07:47:14 +02:00
Román Martínez 93f203bcf1 Track deletion is handled by a service that shows a progress bar on the notification bar. Fixes #735. 2021-05-17 19:45:26 +02:00
Dennis Guse 6b0ec72b9b TrackListActivity: re-enable search.
Fixes #757.
2021-05-16 14:35:10 +02:00
Dennis Guse d46cea6150 Update AndroidManifest.xml 2021-05-16 14:34:42 +02:00
Dennis Guse 0e3ffa58f0 Disable hasFragileUserData for production.
Should fix #685 and #129.
2021-05-16 14:34:42 +02:00
Dennis Guse c00d769188 SensorDataCycling: use Distance. 2021-05-16 08:35:47 +02:00
Dennis Guse 00489a121a Bugfix: TrackStatisticsUpdater was computing distance incorrectly.
While using a cycling speed and distance sensor, the updater was not using the distance from the most recent trackpoint but rather the one considered last moving.

Fixes #762.
2021-05-16 08:35:47 +02:00
Dennis Guse d7f339c6f8 Settings: disable voice announcement setting if not TTS available.
Fixes #763.
2021-05-16 08:34:20 +02:00
Dennis Guse da28ea3e95 Cleanup. 2021-05-16 00:08:15 +02:00
Dennis Guse 9872658f81 Update AndroidStudio config files for 4.2.1 2021-05-14 19:52:12 +02:00
dependabot[bot] b6b155f19c Bump gradle from 4.2.0 to 4.2.1
Bumps gradle from 4.2.0 to 4.2.1.

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-14 19:08:37 +02:00
Dennis Guse 067f31ac21 Bugfix: ControllerFragment did not work in landscape mode.
Introduced in ed6daeb48b
2021-05-13 12:42:05 +02:00
dependabot[bot] 3f0ce27870 Bump mockito-android from 3.9.0 to 3.10.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.9.0 to 3.10.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.9.0...v3.10.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-13 08:19:07 +02:00
Dennis Guse 438f8fe273 TrackListActivity: disable search as it is not working if not shown as Action. 2021-05-13 06:37:24 +02:00
Dennis Guse 392c02a56f Cleanup. 2021-05-13 06:27:16 +02:00
Quang Trung 6dca3bea58 Translated using Weblate (Vietnamese)
Currently translated at 80.3% (426 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2021-05-12 07:40:32 +02:00
mondstern 13034c8bb3 Translated using Weblate (Polish)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pl/
2021-05-12 07:40:32 +02:00
mondstern 1175ce270e Translated using Weblate (Dutch)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nl/
2021-05-12 07:40:32 +02:00
mondstern 02cad98a4f Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2021-05-12 07:40:32 +02:00
mondstern d6b38a390e Translated using Weblate (Italian)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-05-12 07:40:32 +02:00
J. Lavoie 98deed8ef7 Translated using Weblate (Italian)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2021-05-12 07:40:32 +02:00
J. Lavoie 9983168cb9 Translated using Weblate (French)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-05-12 07:40:32 +02:00
mondstern 7b5f29db09 Translated using Weblate (French)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2021-05-12 07:40:32 +02:00
J. Lavoie 49efd04326 Translated using Weblate (Finnish)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fi/
2021-05-12 07:40:32 +02:00
mondstern 3159c62eb2 Translated using Weblate (Spanish)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es/
2021-05-12 07:40:32 +02:00
Max Schallert 0e8811d8c9 Translated using Weblate (German)
Currently translated at 97.5% (517 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
J. Lavoie 4126e36f1c Translated using Weblate (German)
Currently translated at 97.5% (517 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
mondstern b7e2df9f2f Translated using Weblate (Czech)
Currently translated at 100.0% (530 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2021-05-12 07:40:32 +02:00
Max Schallert 055093772d Translated using Weblate (German)
Currently translated at 97.1% (515 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
J. Lavoie 7f8b49814b Translated using Weblate (German)
Currently translated at 97.1% (515 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
Max Schallert 5cc0e06fbb Translated using Weblate (German)
Currently translated at 95.6% (507 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
J. Lavoie 311a2550e5 Translated using Weblate (German)
Currently translated at 95.6% (507 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
J. Lavoie 4311b29833 Translated using Weblate (German)
Currently translated at 95.6% (507 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
Max Schallert 86121ac27a Translated using Weblate (German)
Currently translated at 95.6% (507 of 530 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2021-05-12 07:40:32 +02:00
Dennis Guse ed6daeb48b TrackController is now ControllerFragment. 2021-05-12 07:39:47 +02:00
dependabot[bot] af72960cf6 Bump reactivecircus/android-emulator-runner from 2 to 2.16.0
Bumps [reactivecircus/android-emulator-runner](https://github.com/reactivecircus/android-emulator-runner) from 2 to 2.16.0.
- [Release notes](https://github.com/reactivecircus/android-emulator-runner/releases)
- [Changelog](https://github.com/ReactiveCircus/android-emulator-runner/blob/main/CHANGELOG.md)
- [Commits](https://github.com/reactivecircus/android-emulator-runner/compare/v2...v2.16.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-12 07:39:11 +02:00
dependabot[bot] 99767d0c0b Bump actions/checkout from 2 to 2.3.4
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 2.3.4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v2.3.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-12 07:39:01 +02:00
Dennis Guse 1fc0a552c7 ChartView: improve performance. 2021-05-11 07:42:17 +02:00
Dennis Guse 18dd1de1f9 TrackDataHub: prevent race condition. 2021-05-08 19:31:16 +02:00
63 changed files with 1432 additions and 859 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
with: with:
fetch-depth: 2 fetch-depth: 2
+2 -2
View File
@@ -10,10 +10,10 @@ jobs:
target: [default] target: [default]
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v2 uses: actions/checkout@v2.3.4
- name: run tests - name: run tests
uses: reactivecircus/android-emulator-runner@v2 uses: reactivecircus/android-emulator-runner@v2.16.0
with: with:
api-level: ${{ matrix.api-level }} api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }} target: ${{ matrix.target }}
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">
+1
View File
@@ -3,6 +3,7 @@
<component name="RunConfigurationProducerService"> <component name="RunConfigurationProducerService">
<option name="ignoredProducers"> <option name="ignoredProducers">
<set> <set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" /> <option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" /> <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" /> <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
+45 -36
View File
@@ -49,39 +49,6 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
</tr> </tr>
</table> </table>
## Features:
* __Tracking:__ track your sport and outdoor activities
* __Photos and Markers:__ mark interesting locations while tracking (also with photos)
* __Bluetooth LE sensors:__
* heart rate
* cycling: speed and cadence
* cycling: power meter
* __Export data:__ export tracks either as KMZ (incl. photos), KML or GPX
* __No Internet access:__ Internet is not used
* __Voice announcements__
* __No advertising__
* __Privacy:__
* _does not contain any in-app analytics_
* _does not use Google Play Services_
* _has no cloud integration_
__Only required permissions:__
* _ACCESS_FINE_LOCATION_: required to use the GPS.
An overview of Bluetooth LE sensors that are known to work with OpenTracks is in [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md).
_Please note:_
_OpenTracks_ does not contain a _map_ (as this would require internet access).
## Custom Dashboards (incl. Map)
As of v3.3.1 OpenTracks enables to use custom dashboards.
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates).
The developer documentation is in [README_API.md](README_API.md).
Alternatively, recorded tracks can be shared as KMZ/GPX to installed applications (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand)).
However, this is rather slow and does not provide updates while recording.
## Screenshots ## Screenshots
<div> <div>
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot1.png"> <img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot1.png">
@@ -96,12 +63,54 @@ However, this is rather slow and does not provide updates while recording.
<img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot8.png"> <img width="23%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot8.png">
</div> </div>
## Features
* __Tracking:__ track your sport and outdoor activities
* __Voice announcements__
* __Photos and Markers:__ mark interesting locations while tracking
* __Export:__
* export tracks either as KMZ (incl. photos), KML, or GPX
* export automatically after each recording (e.g., to sync)
* avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`)
* __Altitude:__
* gain/loss via barometric sensor (if present)
* shown in EGM2008 (above mean sea level); exported as WGS84
* __Bluetooth LE sensors:__
* heart rate
* cycling: speed and distance
* cycling: cadence
* cycling: power meter
### Gadgetbridge integration
OpenTracks can be used with [Gadgetbridge](https://www.gadgetbridge.org/):
* shows statistics via notification on smart watches (requires Gadgetbridge 0.56.1 or later), and
* Gadgetbridge's GPX exporter generates `opentracks:trackid` to avoid duplication (Gadgetbridge 0.53.0 or later).
### Privacy
* __No Internet access:__ Internet is not used
* __No advertising__
* __No in-app analytics__
* __No use of Google Play Services__
__Only required permission:__
* _ACCESS_FINE_LOCATION_: required to use the GPS.
An overview of Bluetooth LE sensors that are known to work with OpenTracks is in [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md).
## Custom Dashboards (incl. map)
As of v3.3.1, OpenTracks supports custom dashboards.
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates).
The developer documentation is in [README_API.md](README_API.md).
Alternatively, recorded tracks can be shared as KMZ/GPX with installed applications (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand)).
However, this is rather slow and does not provide updates while recording.
## Project history ## Project history
_OpenTracks_ is based upon Google _My Tracks app_ ([code](https://code.google.com/archive/p/mytracks/)). _OpenTracks_ is based upon Google _My Tracks app_ ([code](https://code.google.com/archive/p/mytracks/)).
Initially, _My Tracks_ was initially released by Google in 2010 as [open-source software](http://google-latlong.blogspot.fr/2010/05/code-for-my-tracks-is-now-yours.html). Initially, _My Tracks_ was initially released by Google in 2010 as [open-source software](http://google-latlong.blogspot.fr/2010/05/code-for-my-tracks-is-now-yours.html).
In 2016, [Google decided to discontinue](https://support.google.com/maps/answer/6333516) _My Tracks_ and stopped distributing it via the Google Play store in April 2016. In 2016, [Google decided to discontinue](https://support.google.com/maps/answer/6333516) _My Tracks_ and stopped distributing it via the Google Play store in April 2016.
The [Plonk42](https://github.com/plonk42) conducted some maintenance work until 2016, so _My Tracks_ could still be used (based upon version _Google's MyTracks_ version 2.0.6). Then [Plonk42](https://github.com/plonk42) conducted some maintenance work until 2016, so _My Tracks_ could still be used (based upon version _Google's MyTracks_ version 2.0.6).
Plonk42's version is available [here](https://github.com/Plonk42/mytracks). Plonk42's version is available [here](https://github.com/Plonk42/mytracks).
In 2019, _OpenTracks_ was forked from Plonk42's _My Tracks_ and major rework was conducted. In 2019, _OpenTracks_ was forked from Plonk42's _My Tracks_ and major rework was conducted.
@@ -114,11 +123,11 @@ Rework of _OpenTracks_ included:
* removing calorie estimation and activity estimation, * removing calorie estimation and activity estimation,
* removing support for ANT+ and Classic Bluetooth, * removing support for ANT+ and Classic Bluetooth,
* adding support for Bluetooth LE heart rate sensors, * adding support for Bluetooth LE heart rate sensors,
* removing Protobuf (store sensosensorr data in SQLite columns directly), and * removing Protobuf (store sensor data in SQLite columns directly), and
* removing Android Service API for other apps. * removing Android Service API for other apps.
Artwork, logos and user interface remained more or less unchanged. Artwork, logos and user interface remained more or less unchanged.
More information about _Google My Tracks_: More information about _Google My Tracks_:
* [Wikipedia page](https://en.wikipedia.org/wiki/MyTracks) * [Wikipedia page](https://en.wikipedia.org/wiki/MyTracks)
* [Google code archive](https://code.google.com/archive/p/mytracks/) * [Google code archive](https://code.google.com/archive/p/mytracks/)
+5 -5
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.2.0' classpath 'com.android.tools.build:gradle:4.2.1'
} }
} }
@@ -55,8 +55,8 @@ android {
defaultConfig { defaultConfig {
applicationId "de.dennisguse.opentracks" applicationId "de.dennisguse.opentracks"
versionCode 4046 versionCode 4092
versionName "v3.18.0" versionName "v3.18.1"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\"" buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -93,7 +93,7 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha06' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha06'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'com.google.android.material:material:1.3.0' implementation 'com.google.android.material:material:1.3.0'
@@ -103,5 +103,5 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'org.mockito:mockito-android:3.9.0' androidTestImplementation 'org.mockito:mockito-android:3.10.0'
} }
@@ -0,0 +1,13 @@
v3.18.1: OpenTracks
Changes:
- Deleting tracks is handled by a service
- Uninstall deletes all data (i.e., hasFragileData=false)
Bugfixes:
- Race condition: crash while recording and switching OpenTracks into background
- Search in track list was not working
- Cycling speed sensor: distance computation was wrong (only affected statistics)
Developer:
- ControllerFragment replaces TrackController
@@ -3,9 +3,9 @@ OpenTracks is a sport tracking application that completely respects your privacy
Features: Features:
* Sport tracking: track your sport and outdoor activities * Sport tracking: track your sport and outdoor activities
* Photos and markers: mark interesting locations while tracking * Photos and markers: mark interesting locations while tracking
* Supports Bluetooth LE sensors: * Altitude gain and loss: via barometric sensor
* heart rate * Altitude shown in EGM2008 (above mean sea level); exported as WGS84
* cycling: speed and cadence * Voice announcements
* cycling: power meter * Supports Bluetooth LE sensors: heart rate, speed and distance (cycling), cadence (cycling), and power meter (cycling)
* Export data: export tracks either as KML or GPX * Export data: export tracks either as KMZ (incl. photos), KML, or GPX
* No Internet access: Internet is not used * No Internet access: Internet is not used
@@ -0,0 +1,285 @@
package de.dennisguse.opentracks;
import android.app.ActivityManager;
import android.app.Instrumentation;
import android.content.Context;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.ListView;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.IdlingResource;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.action.MotionEvents;
import androidx.test.espresso.assertion.ViewAssertions;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.services.TrackDeleteService;
import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.pressBack;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoDeleteTrackTest {
@Rule
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant("android.permission.ACCESS_FINE_LOCATION");
private MyIdlingResource idlingResource;
@Before
public void registerIntentServiceIdlingResource() {
Instrumentation instrumentation
= InstrumentationRegistry.getInstrumentation();
idlingResource = new MyIdlingResource(instrumentation.getTargetContext());
Espresso.registerIdlingResources(idlingResource);
}
@After
public void unregisterIntentServiceIdlingResource() {
Espresso.unregisterIdlingResources(idlingResource);
}
@Test
public void espressoDeleteTrackTest() {
int countBefore;
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
trackControllerRecordButton.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
// wait; stay recording
trackControllerRecordButton.perform(waitFor(5000));
// stop;
trackControllerStopButton.perform(veryLongTouch(1600));
}
// back
pressBack();
// get number of items before deleting a track
countBefore = numberOfItemsListView();
// select track
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(veryLongTouch(5000));
// open menu and delete selected track
ViewInteraction overflowMenuButton = onView(
allOf(withContentDescription("More options"),
childAtPosition(
childAtPosition(
withId(R.id.toolbar),
2),
2),
isDisplayed()));
overflowMenuButton.perform(click());
ViewInteraction appCompatTextView = onView(
allOf(withId(R.id.title), withText("Delete"),
childAtPosition(
childAtPosition(
withId(R.id.content),
0),
0),
isDisplayed()));
appCompatTextView.perform(click());
ViewInteraction appCompatButton = onView(
allOf(withId(android.R.id.button1), withText("Yes"),
childAtPosition(
childAtPosition(
withId(R.id.buttonPanel),
0),
3)));
appCompatButton.perform(scrollTo(), click());
// check number of items after delete
onView(withId(R.id.track_list)).check(ViewAssertions.matches(withListSize(countBefore - 1)));
}
private static Matcher<View> childAtPosition(
final Matcher<View> parentMatcher, final int position) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("Child at position " + position + " in parent ");
parentMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent)
&& view.equals(((ViewGroup) parent).getChildAt(position));
}
};
}
public static Matcher<View> withListSize (final int size) {
return new TypeSafeMatcher<View> () {
@Override public boolean matchesSafely (final View view) {
return ((ListView) view).getCount () == size;
}
@Override public void describeTo (final Description description) {
description.appendText ("ListView should have " + size + " items");
}
};
}
private static ViewAction waitFor(final long duration_ms) {
return new ViewAction() {
@Override
public String getDescription() {
return "Wait for milliseconds.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
uiController.loopMainThreadForAtLeast(duration_ms);
}
};
}
private static ViewAction veryLongTouch(final int duration_ms) {
return new ViewAction() {
@Override
public String getDescription() {
return "Perform long touch.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
// Get view absolute position
int[] location = new int[2];
view.getLocationOnScreen(location);
// Offset coordinates by view position
float[] coordinates = new float[]{location[0] + 1, location[1] + 1};
// Send down event, pause, and send up
MotionEvent down = MotionEvents.sendDown(uiController, coordinates, new float[]{1f, 1f}).down;
uiController.loopMainThreadForAtLeast(duration_ms);
MotionEvents.sendUp(uiController, down, coordinates);
}
};
}
private int numberOfItemsListView() {
final int[] counts = new int[1];
onView(withId(R.id.track_list)).check(matches(new TypeSafeMatcher<View>() {
@Override
public boolean matchesSafely(View view) {
ListView listView = (ListView) view;
counts[0] = listView.getCount();
return true;
}
@Override
public void describeTo(Description description) {
}
}));
return counts[0];
}
private class MyIdlingResource implements IdlingResource {
private Context context;
private ResourceCallback resourceCallback;
public MyIdlingResource(Context context) {
this.context = context;
}
@Override
public String getName() {
return MyIdlingResource.class.getName();
}
@Override
public void registerIdleTransitionCallback(IdlingResource.ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
@Override
public boolean isIdleNow() {
boolean idle = !isDeleteServiceRunning();
if (idle && resourceCallback != null) {
resourceCallback.onTransitionToIdle();
}
return idle;
}
private boolean isDeleteServiceRunning() {
ActivityManager manager =
(ActivityManager) context.getSystemService(
Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningServiceInfo info :
manager.getRunningServices(Integer.MAX_VALUE)) {
if (TrackDeleteService.class.getName().equals(
info.service.getClassName())) {
return true;
}
}
return false;
}
}
}
@@ -19,10 +19,13 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.Matchers.anything;
@LargeTest @LargeTest
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@@ -39,13 +42,13 @@ public class EspressoUITest {
public void record_pause_resume_stop() { public void record_pause_resume_stop() {
{ {
// TrackListActivity: start recording // TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record)); ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
trackControllerRecordButton.perform(click()); trackControllerRecordButton.perform(click());
} }
{ {
// TrackRecordingActivity // TrackRecordingActivity
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record)); ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_controller_stop)); ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
// wait; stay recording // wait; stay recording
trackControllerRecordButton.perform(waitFor(5000)); trackControllerRecordButton.perform(waitFor(5000));
@@ -72,13 +75,13 @@ public class EspressoUITest {
public void record_move_through_tabs() { public void record_move_through_tabs() {
{ {
// TrackListActivity: start recording // TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record)); ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
trackControllerRecordButton.perform(click()); trackControllerRecordButton.perform(click());
} }
{ {
// TrackRecordingActivity // TrackRecordingActivity
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout)); ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_controller_stop)); ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
tabLayout.perform(selectTabAtIndex(1)); tabLayout.perform(selectTabAtIndex(1));
tabLayout.perform(waitFor(1000)); tabLayout.perform(waitFor(1000));
@@ -97,6 +100,13 @@ public class EspressoUITest {
} }
} }
@LargeTest
@Test
public void selectAndDeleteTrack() {
onView(withId(R.id.track_list)).check(matches(isDisplayed()));
onData(anything()).inAdapterView(withId(R.id.track_list)).atPosition(0).perform(veryLongTouch(2000));
}
private static ViewAction veryLongTouch(final int duration_ms) { private static ViewAction veryLongTouch(final int duration_ms) {
return new ViewAction() { return new ViewAction() {
@Override @Override
@@ -1,10 +1,13 @@
package de.dennisguse.opentracks.content.data; package de.dennisguse.opentracks.content.data;
import android.location.Location;
import org.junit.Test; import org.junit.Test;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@@ -25,4 +28,35 @@ public class TrackPointTest {
assertFalse(tp.isRecent()); assertFalse(tp.isRecent());
} }
@Test
public void distanceToPrevious() {
Location l1 = new Location("test");
l1.setLatitude(0);
l1.setLongitude(0.0001);
TrackPoint tp1 = new TrackPoint(l1);
Location l2 = new Location("test");
l2.setLatitude(0);
l2.setLongitude(0.0002);
TrackPoint tp2 = new TrackPoint(l2);
// without sensor distance
assertEquals(11.13, tp2.distanceToPrevious(tp1).toM(), 0.01);
// tp1 has sensor distance
tp1.setSensorDistance(Distance.of(5));
tp1.setSensorDistance(null);
assertEquals(11.13, tp2.distanceToPrevious(tp1).toM(), 0.01);
// tp2 has sensor distance
tp1.setSensorDistance(null);
tp2.setSensorDistance(Distance.of(5));
assertEquals(5, tp2.distanceToPrevious(tp1).toM(), 0.01);
// tp1 and tp2 have sensor distance
tp1.setSensorDistance(Distance.of(10));
tp2.setSensorDistance(Distance.of(5));
assertEquals(5, tp2.distanceToPrevious(tp1).toM(), 0.01);
}
} }
@@ -5,6 +5,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.util.UintUtils; import de.dennisguse.opentracks.util.UintUtils;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -100,7 +101,7 @@ public class SensorDataCyclingTest {
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 2, 8016); SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 2, 8016);
// when // when
current.compute(previous, 2150); current.compute(previous, Distance.ofMM(2150));
// then // then
assertEquals(2.15, current.getValue().distance.toM(), 0.01); assertEquals(2.15, current.getValue().distance.toM(), 0.01);
@@ -114,7 +115,7 @@ public class SensorDataCyclingTest {
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048); SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
// when // when
current.compute(previous, 2000); current.compute(previous, Distance.ofMM(2000));
// then // then
assertEquals(2, current.getValue().distance.toM(), 0.01); assertEquals(2, current.getValue().distance.toM(), 0.01);
@@ -146,6 +146,34 @@ public class TrackStatisticsUpdaterTest {
assertEquals(15, subject.getTrackStatistics().getTotalDistance().toM(), 0.01); assertEquals(15, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
} }
@Test
public void addTrackPoint_distance_from_GPS_moving_and_sensor_disconnecting() {
// given
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(3000));
tp3.setSensorDistance(Distance.of(5f));
TrackPoint tp4 = new TrackPoint(0.0005, 0, Altitude.WGS84.of(5.0), Instant.ofEpochMilli(4000));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
// when
subject.addTrackPoint(tp1, GPS_DISTANCE);
subject.addTrackPoint(tp2, GPS_DISTANCE);
subject.addTrackPoint(tp3, GPS_DISTANCE);
// then
assertEquals(5, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
// when
subject.addTrackPoint(tp4, GPS_DISTANCE);
subject.addTrackPoint(tp5, GPS_DISTANCE);
// then
assertEquals(59.18, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
}
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor") @Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@Test @Test
public void addTrackPoint_speed_from_GPS_not_moving() { public void addTrackPoint_speed_from_GPS_not_moving() {
+7
View File
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:hasFragileUserData="true"
tools:replace="android:hasFragileUserData" />
</manifest>
+8 -5
View File
@@ -51,7 +51,7 @@ limitations under the License.
android:name=".Startup" android:name=".Startup"
android:allowBackup="false" android:allowBackup="false"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:hasFragileUserData="true" android:hasFragileUserData="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
@@ -59,10 +59,6 @@ limitations under the License.
<activity android:name=".AboutActivity" /> <activity android:name=".AboutActivity" />
<activity
android:name=".TrackDeleteActivity"
android:theme="@style/TrackDeleteActivity" />
<activity <activity
android:name=".HelpActivity" android:name=".HelpActivity"
android:label="@string/menu_help" /> android:label="@string/menu_help" />
@@ -350,6 +346,13 @@ limitations under the License.
android:name=".io.file.importer.ImportService" android:name=".io.file.importer.ImportService"
android:exported="false" android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" /> android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".services.TrackDeleteService"
android:enabled="true"
android:exported="false"
android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/delete_service" />
</application> </application>
<queries> <queries>
@@ -16,8 +16,6 @@
package de.dennisguse.opentracks; package de.dennisguse.opentracks;
import android.content.Intent;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
@@ -26,33 +24,38 @@ import java.util.Arrays;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment; import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller; import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
import de.dennisguse.opentracks.services.TrackDeleteService;
import de.dennisguse.opentracks.services.TrackDeleteServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.IntentUtils;
/** /**
* An abstract class for the following common tasks across * An abstract class for the following common tasks across
* {@link TrackListActivity} and {@link TrackRecordedActivity}: * {@link TrackListActivity} and {@link TrackRecordedActivity}:
* <p> * <p>
* - share track <br>
* - delete tracks <br> * - delete tracks <br>
* *
* @author Jimmy Shih * @author Jimmy Shih
*/ */
//TODO It is actually not a List; we need a better name for this class
//TODO Check if this class is still such a good idea; inheritance might limit maintainability //TODO Check if this class is still such a good idea; inheritance might limit maintainability
public abstract class AbstractListActivity extends AbstractActivity implements ConfirmDeleteCaller { public abstract class AbstractTrackDeleteActivity extends AbstractActivity implements ConfirmDeleteCaller, TrackDeleteServiceConnection.Listener {
private static final String TAG = AbstractListActivity.class.getSimpleName(); private static final String TAG = AbstractTrackDeleteActivity.class.getSimpleName();
protected static final int GPS_REQUEST_CODE = 6; private TrackDeleteServiceConnection trackDeleteServiceConnection;
private static final int DELETE_REQUEST_CODE = 3;
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onStart() {
if (requestCode == DELETE_REQUEST_CODE) { super.onStart();
onTrackDeleted(); trackDeleteServiceConnection = new TrackDeleteServiceConnection(this);
} else { trackDeleteServiceConnection.bind(this);
super.onActivityResult(requestCode, resultCode, data); }
@Override
protected void onStop() {
super.onStop();
if (trackDeleteServiceConnection != null) {
trackDeleteServiceConnection.unbind(this);
trackDeleteServiceConnection = null;
} }
} }
@@ -69,6 +72,8 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
public void onConfirmDeleteDone(Track.Id... trackIds) { public void onConfirmDeleteDone(Track.Id... trackIds) {
boolean stopRecording = false; boolean stopRecording = false;
onDeleteConfirmed();
for (Track.Id trackId : trackIds) { for (Track.Id trackId : trackIds) {
if (trackId.equals(getRecordingTrackId())) { if (trackId.equals(getRecordingTrackId())) {
stopRecording = true; stopRecording = true;
@@ -79,10 +84,9 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
if (stopRecording) { if (stopRecording) {
getTrackRecordingServiceConnection().stopRecording(this); getTrackRecordingServiceConnection().stopRecording(this);
} }
Intent intent = IntentUtils.newIntent(this, TrackDeleteActivity.class);
intent.putParcelableArrayListExtra(TrackDeleteActivity.EXTRA_TRACK_IDS, new ArrayList<>(Arrays.asList(trackIds))); trackDeleteServiceConnection = new TrackDeleteServiceConnection(this);
startActivityForResult(intent, DELETE_REQUEST_CODE); trackDeleteServiceConnection.startAndBind(this, new ArrayList<>(Arrays.asList(trackIds)));
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
} }
/** /**
@@ -94,12 +98,28 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
} }
/** /**
* Called after {@link TrackDeleteActivity} returns its result. * Called every time a track is deleted.
*/ */
abstract protected void onTrackDeleted(); protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
if (deleteStatus.isFinished() && trackDeleteServiceConnection != null) {
trackDeleteServiceConnection.unbind(this);
trackDeleteServiceConnection = null;
}
}
protected abstract void onDeleteConfirmed();
@Nullable @Nullable
protected Track.Id getRecordingTrackId() { protected Track.Id getRecordingTrackId() {
return null; return null;
} }
@Override
public void connected() {
TrackDeleteService service = trackDeleteServiceConnection.getServiceIfBound();
if (service == null) {
return;
}
service.getDeletingStatusObservable().observe(AbstractTrackDeleteActivity.this, status -> onTrackDeleteStatus(status));
}
} }
@@ -0,0 +1,299 @@
/*
* 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.annotation.SuppressLint;
import android.graphics.drawable.TransitionDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import java.time.Duration;
import de.dennisguse.opentracks.databinding.ControllerFragmentBinding;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Track controller for record, pause, resume, and stop.
*
* @author Jimmy Shih
*/
//TODO Move buttons into separate components
public class ControllerFragment extends Fragment implements View.OnTouchListener {
private static final String TAG = ControllerFragment.class.getSimpleName();
@Deprecated
//TODO TrackRecordingService should provide LifeData for time (replace internal timer)
private static final Duration UI_UPDATE_INTERVAL = Duration.ofSeconds(1);
@Deprecated
private Handler handlerUpdateTotalTime;
private Runnable buttonDelay;
private int buttonDelayDuration;
private ControllerFragmentBinding viewBinding;
private TransitionDrawable transition;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackRecordingService.RecordingStatus recordingStatus;
private Duration totalTime;
private final Runnable bindChangedCallback = new Runnable() {
@Override
public void run() {
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.w(TAG, "could not get TrackRecordingService");
return;
}
service.getRecordingStatusObservable()
.observe(ControllerFragment.this, status -> onRecordingStatusChanged(status));
}
};
// A runnable to update the total time.
@Deprecated
private final Runnable updateTotalTimeRunnable = new Runnable() {
public void run() {
if (isResumed() && recordingStatus.isRecordingAndNotPaused()) {
updateTotalTime();
setTotalTime();
handlerUpdateTotalTime.postDelayed(this, UI_UPDATE_INTERVAL.toMillis());
}
}
};
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
buttonDelayDuration = getResources().getInteger(R.integer.buttonDelayMillis);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
handlerUpdateTotalTime = new Handler();
recordingStatus = TrackRecordingService.STATUS_DEFAULT;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
viewBinding = ControllerFragmentBinding.inflate(inflater, container, false);
viewBinding.controllerRecord.setOnTouchListener(this);
viewBinding.controllerRecord.setOnClickListener((view) -> {
if (buttonDelay != null || recordingStatus.isRecordingAndNotPaused()) {
return;
}
getCallback().recordStart();
});
viewBinding.controllerStop.setOnTouchListener(this);
transition = (TransitionDrawable) viewBinding.controllerContainer.getBackground();
onRecordingStatusChanged(recordingStatus);
return viewBinding.getRoot();
}
@Override
public void onResume() {
super.onResume();
trackRecordingServiceConnection.startConnection(getContext());
}
@Override
public void onPause() {
super.onPause();
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
}
@Override
public void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind(getContext());
}
@Override
public void onDestroyView() {
super.onDestroyView();
viewBinding = null;
transition = null;
}
@Override
public void onDestroy() {
super.onDestroy();
trackRecordingServiceConnection = null;
handlerUpdateTotalTime = null;
recordingStatus = null;
}
//TODO Move into a separate object
@SuppressLint("ClickableViewAccessibility") //We do accessibility manually.
@Override
public boolean onTouch(final View view, MotionEvent event) {
if (!isResumed()) {
Log.w(TAG, "The UI is already gone, no need to update anything; seen on Android 7.1.2");
return false;
}
if (event.getAction() == MotionEvent.ACTION_MOVE) {
return false;
}
if (buttonDelay != null && event.getAction() == MotionEvent.ACTION_UP) {
handlerUpdateTotalTime.removeCallbacks(buttonDelay);
buttonDelay = null;
view.setPressed(false);
transition.resetTransition();
showStatusSetDefaultText();
return false;
}
//Trigger button pressed animation
view.setPressed(true);
//To pause a recording
if (viewBinding.controllerRecord.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& recordingStatus.isRecordingAndNotPaused()) {
transition.startTransition(buttonDelayDuration);
ActivityUtils.vibrate(getContext(), 150);
viewBinding.controllerStatus.setText(R.string.hold_to_pause);
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
view.performClick();
getCallback().recordPause();
transition.resetTransition();
ActivityUtils.vibrate(getContext(), 1000);
};
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
return true;
}
//To stop a recording
if (viewBinding.controllerStop.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& recordingStatus.isRecording()) {
transition.startTransition(buttonDelayDuration);
ActivityUtils.vibrate(getContext(), 150);
viewBinding.controllerStatus.setText(R.string.hold_to_stop);
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
getCallback().recordStop();
ActivityUtils.vibrate(getContext(), 1000);
transition.resetTransition();
};
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
return true;
}
return false;
}
public void hide() {
viewBinding.controllerContainer.setVisibility(View.GONE);
}
public void show() {
viewBinding.controllerContainer.setVisibility(View.VISIBLE);
}
private void updateTotalTime() {
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
totalTime = trackRecordingService.getTotalTime();
}
}
private void setTotalTime() {
if (totalTime != null) {
viewBinding.controllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
}
}
private void showStatusSetDefaultText() {
viewBinding.controllerStatus.setText(recordingStatus.isPaused() ? R.string.generic_paused : R.string.generic_recording);
}
private Callback getCallback() {
return (Callback) getActivity();
}
public interface Callback {
void recordStart();
void recordPause();
void recordStop();
}
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status;
if (!recordingStatus.isRecording()) {
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
}
if (recordingStatus.isRecordingAndNotPaused()) {
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UI_UPDATE_INTERVAL.toMillis());
}
viewBinding.controllerRecord.setImageResource(recordingStatus.isRecordingAndNotPaused() ? R.drawable.ic_button_pause : R.drawable.button_record);
viewBinding.controllerRecord.setContentDescription(getContext().getString(recordingStatus.isRecordingAndNotPaused() ? R.string.image_pause : R.string.image_record));
viewBinding.controllerStop.setEnabled(recordingStatus.isRecording());
viewBinding.controllerStatus.setVisibility(recordingStatus.isRecording() ? View.VISIBLE : View.INVISIBLE);
if (recordingStatus.isRecording()) {
viewBinding.controllerStatus.setTextColor(getContext().getResources().getColor(recordingStatus.isPaused() ? android.R.color.white : R.color.recording_text));
showStatusSetDefaultText();
}
viewBinding.controllerTotalTime.setVisibility(recordingStatus.isRecording() ? View.VISIBLE : View.INVISIBLE);
if (recordingStatus.isRecording()) {
updateTotalTime();
setTotalTime();
}
}
}
@@ -175,7 +175,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker); insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
searchMenuItem = menu.findItem(R.id.marker_list_search); searchMenuItem = menu.findItem(R.id.marker_list_search);
ActivityUtils.configureSearchWidget(this, searchMenuItem, null); ActivityUtils.configureSearchWidget(this, searchMenuItem);
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@@ -261,15 +261,16 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
@Override @Override
public void onBackPressed() { public void onBackPressed() {
SearchView searchView = (SearchView) searchMenuItem.getActionView();
if (!searchView.isIconified()) {
searchView.setIconified(true);
}
if (loaderCallbacks.getSearchQuery() != null) { if (loaderCallbacks.getSearchQuery() != null) {
loaderCallbacks.setSearch(null); loaderCallbacks.setSearch(null);
return; return;
} }
SearchView searchView = (SearchView) searchMenuItem.getActionView();
if (!searchView.isIconified()) {
searchView.setIconified(true);
return;
}
super.onBackPressed(); super.onBackPressed();
} }
@@ -1,260 +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.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.drawable.TransitionDrawable;
import android.os.Handler;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import java.time.Duration;
import de.dennisguse.opentracks.databinding.TrackControllerBinding;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Track controller for record, pause, resume, and stop.
*
* @author Jimmy Shih
*/
//TODO Subscribe to TrackRecordingService.recordingStatus
//TODO Could be a fragment
public class TrackController implements View.OnTouchListener {
private static final String TAG = TrackController.class.getSimpleName();
private static final Duration UI_UPDATE_INTERVAL = Duration.ofSeconds(1);
private final Activity activity;
private final TrackRecordingServiceConnection trackRecordingServiceConnection;
private final Handler handlerUpdateTotalTime = new Handler();
private final boolean alwaysShow;
private Runnable buttonDelay;
private final int buttonDelayDuration;
private final TrackControllerBinding viewBinding;
private final TransitionDrawable transition;
private TrackRecordingService.RecordingStatus recordingStatus;
private Duration totalTime;
private final Callback callback;
// A runnable to update the total time.
private final Runnable updateTotalTimeRunnable = new Runnable() {
public void run() {
if (isResumed() && recordingStatus.isRecordingAndNotPaused()) {
updateTotalTime();
setTotalTime();
handlerUpdateTotalTime.postDelayed(this, UI_UPDATE_INTERVAL.toMillis());
}
}
};
@SuppressLint("ClickableViewAccessibility")
TrackController(Activity activity, TrackControllerBinding viewBinding, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, Callback callback) {
this.activity = activity;
this.viewBinding = viewBinding;
this.trackRecordingServiceConnection = trackRecordingServiceConnection;
this.alwaysShow = alwaysShow;
this.callback = callback;
buttonDelayDuration = activity.getResources().getInteger(R.integer.buttonDelayMillis);
viewBinding.trackControllerRecord.setOnTouchListener(this);
viewBinding.trackControllerRecord.setOnClickListener((view) -> {
if (buttonDelay != null || recordingStatus.isRecordingAndNotPaused()) {
return;
}
callback.recordStart();
});
viewBinding.trackControllerStop.setOnTouchListener(this);
transition = (TransitionDrawable) viewBinding.trackControllerContainer.getBackground();
}
@SuppressLint("ClickableViewAccessibility") //We do accessibility manually.
@Override
public boolean onTouch(final View view, MotionEvent event) {
if (!isResumed()) {
Log.w(TAG, "The UI is already gone, no need to update anything; seen on Android 7.1.2");
return false;
}
if (event.getAction() == MotionEvent.ACTION_MOVE) {
return false;
}
if (buttonDelay != null && event.getAction() == MotionEvent.ACTION_UP) {
handlerUpdateTotalTime.removeCallbacks(buttonDelay);
buttonDelay = null;
view.setPressed(false);
transition.resetTransition();
showStatusSetDefaultText();
return false;
}
//Trigger button pressed animation
view.setPressed(true);
//To pause a recording
if (viewBinding.trackControllerRecord.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& recordingStatus.isRecordingAndNotPaused()) {
transition.startTransition(buttonDelayDuration);
ActivityUtils.vibrate(activity, 150);
viewBinding.trackControllerStatus.setText(R.string.hold_to_pause);
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
view.performClick();
callback.recordPause();
transition.resetTransition();
ActivityUtils.vibrate(activity, 1000);
};
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
return true;
}
//To stop a recording
if (viewBinding.trackControllerStop.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& recordingStatus.isRecording()) {
transition.startTransition(buttonDelayDuration);
ActivityUtils.vibrate(activity, 150);
viewBinding.trackControllerStatus.setText(R.string.hold_to_stop);
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
callback.recordStop();
ActivityUtils.vibrate(activity, 1000);
transition.resetTransition();
};
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
return true;
}
return false;
}
public void update(TrackRecordingService.RecordingStatus recordingStatus) {
if (!isResumed()) {
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
return;
}
this.recordingStatus = recordingStatus;
boolean isRecording = recordingStatus.isRecording();
boolean isRecordingPaused = recordingStatus.isPaused();
boolean visible = alwaysShow || isRecording;
viewBinding.trackControllerContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
if (!visible) {
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
return;
}
if (isRecording && !isRecordingPaused) {
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UI_UPDATE_INTERVAL.toMillis());
}
viewBinding.trackControllerRecord.setImageResource(isRecording && !isRecordingPaused ? R.drawable.ic_button_pause : R.drawable.button_record);
viewBinding.trackControllerRecord.setContentDescription(activity.getString(isRecording && !isRecordingPaused ? R.string.image_pause : R.string.image_record));
viewBinding.trackControllerStop.setEnabled(isRecording);
viewBinding.trackControllerStatus.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
viewBinding.trackControllerStatus.setTextColor(activity.getResources().getColor(isRecordingPaused ? android.R.color.white : R.color.recording_text));
showStatusSetDefaultText();
}
viewBinding.trackControllerTotalTime.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (recordingStatus.isRecording()) {
updateTotalTime();
setTotalTime();
}
}
void onResume(TrackRecordingService.RecordingStatus recordingStatus) {
this.recordingStatus = recordingStatus;
update(recordingStatus);
}
void onPause() {
recordingStatus = null;
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
}
public void hide() {
viewBinding.trackControllerContainer.setVisibility(View.GONE);
}
public void show() {
viewBinding.trackControllerContainer.setVisibility(View.VISIBLE);
}
private void updateTotalTime() {
TrackRecordingService trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
totalTime = trackRecordingService.getTotalTime();
}
}
private void setTotalTime() {
if (totalTime != null) {
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
}
}
private void showStatusSetDefaultText() {
viewBinding.trackControllerStatus.setText(recordingStatus.isPaused() ? R.string.generic_paused : R.string.generic_recording);
}
private boolean isResumed() {
return recordingStatus != null;
}
public interface Callback {
void recordStart();
void recordPause();
void recordStop();
}
}
@@ -1,101 +0,0 @@
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks;
import android.content.Intent;
import android.os.Bundle;
import android.os.PowerManager;
import android.view.View;
import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.TrackDeleteBinding;
import de.dennisguse.opentracks.util.SystemUtils;
/**
* An activity for deleting tracks.
*
* @author Jimmy Shih
*/
public class TrackDeleteActivity extends AbstractActivity {
public static final String EXTRA_TRACK_IDS = "track_ids";
private List<Track.Id> trackIds;
private Thread deleteThread;
private TrackDeleteBinding viewBinding;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setResult(RESULT_CANCELED);
Intent intent = getIntent();
trackIds = intent.getParcelableArrayListExtra(EXTRA_TRACK_IDS);
deleteThread = new Thread(() -> {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(TrackDeleteActivity.this);
PowerManager.WakeLock wakeLock = SystemUtils.acquireWakeLock(TrackDeleteActivity.this, null);
contentProviderUtils.deleteTracks(TrackDeleteActivity.this, trackIds);
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
if (Thread.interrupted()) {
return;
}
runOnUiThread(TrackDeleteActivity.this::onAsyncTaskCompleted);
});
deleteThread.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
viewBinding = null;
}
@Override
protected void onStart() {
super.onStart();
if (deleteThread.getState() == Thread.State.TERMINATED) {
onAsyncTaskCompleted();
}
}
@Override
protected void onStop() {
super.onStop();
deleteThread.interrupt();
}
@Override
protected View getRootView() {
viewBinding = TrackDeleteBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
}
public void onAsyncTaskCompleted() {
viewBinding.progressbar.setVisibility(View.INVISIBLE);
setResult(RESULT_OK);
finish();
}
}
@@ -71,14 +71,15 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackController.Callback { public class TrackListActivity extends AbstractTrackDeleteActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, ControllerFragment.Callback {
private static final String TAG = TrackListActivity.class.getSimpleName(); private static final String TAG = TrackListActivity.class.getSimpleName();
protected static final int GPS_REQUEST_CODE = 6;
// The following are set in onCreate // The following are set in onCreate
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection; private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackController trackController;
private ResourceCursorAdapter resourceCursorAdapter; private ResourceCursorAdapter resourceCursorAdapter;
private TrackListBinding viewBinding; private TrackListBinding viewBinding;
@@ -118,7 +119,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
runOnUiThread(() -> { runOnUiThread(() -> {
TrackListActivity.this.invalidateOptionsMenu(); TrackListActivity.this.invalidateOptionsMenu();
loaderCallbacks.restart(); loaderCallbacks.restart();
trackController.onResume(recordingStatus);
}); });
} }
} }
@@ -132,9 +132,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
private final Runnable bindChangedCallback = new Runnable() { private final Runnable bindChangedCallback = new Runnable() {
@Override @Override
public void run() { public void run() {
// After binding changes (e.g., becomes available), update the total time in trackController.
runOnUiThread(() -> trackController.update(recordingStatus));
TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound(); TrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) { if (service == null) {
Log.e(TAG, "service not available to start gps or a new recording"); Log.e(TAG, "service not available to start gps or a new recording");
@@ -170,14 +167,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
sharedPreferences = PreferencesUtils.getSharedPreferences(this); sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback); trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, true, this);
// Show trackController when search dialog is dismissed
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
if (searchManager != null) {
searchManager.setOnDismissListener(() -> trackController.show());
}
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView); viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> { viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> {
@@ -250,14 +239,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackRecordingServiceConnection.startConnection(this); trackRecordingServiceConnection.startConnection(this);
} }
@Override
protected void onPause() {
super.onPause();
// Update UI
trackController.onPause();
}
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
@@ -265,7 +246,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
// Update UI // Update UI
this.invalidateOptionsMenu(); this.invalidateOptionsMenu();
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks); LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
trackController.onResume(recordingStatus);
} }
@Override @Override
@@ -298,7 +278,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
getMenuInflater().inflate(R.menu.track_list, menu); getMenuInflater().inflate(R.menu.track_list, menu);
searchMenuItem = menu.findItem(R.id.track_list_search); searchMenuItem = menu.findItem(R.id.track_list_search);
ActivityUtils.configureSearchWidget(this, searchMenuItem, trackController); SearchView searchView = ActivityUtils.configureSearchWidget(this, searchMenuItem);
searchView.setOnCloseListener(() -> {
searchView.clearFocus();
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return true;
});
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps); startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
@@ -306,7 +292,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
updateMenuItems(gpsStatusValue.isGpsStarted(), recordingStatus.isRecording()); updateGpsMenuItem(gpsStatusValue.isGpsStarted(), recordingStatus.isRecording());
SearchView searchView = (SearchView) searchMenuItem.getActionView(); SearchView searchView = (SearchView) searchMenuItem.getActionView();
searchView.setQuery("", false); searchView.setQuery("", false);
@@ -355,6 +341,12 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
return true; return true;
} }
if (item.getItemId() == R.id.track_list_search) {
SearchView searchView = (SearchView) searchMenuItem.getActionView();
searchView.setIconified(false);
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@@ -366,17 +358,23 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
return super.onKeyUp(keyCode, event); return super.onKeyUp(keyCode, event);
} }
@Override
public void overridePendingTransition(int enterAnim, int exitAnim) {
//Disable animations as it is weird going into searchMode; looks okay for SplashScreen.
}
@Override @Override
public void onBackPressed() { public void onBackPressed() {
SearchView searchView = (SearchView) searchMenuItem.getActionView();
if (!searchView.isIconified()) {
searchView.setIconified(true);
}
if (loaderCallbacks.getSearchQuery() != null) { if (loaderCallbacks.getSearchQuery() != null) {
loaderCallbacks.setSearch(null); loaderCallbacks.setSearch(null);
return; return;
} }
SearchView searchView = (SearchView) searchMenuItem.getActionView();
if (!searchView.isIconified()) {
searchView.setIconified(true);
return;
}
super.onBackPressed(); super.onBackPressed();
} }
@@ -396,20 +394,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
loaderCallbacks.setSearch(searchQuery); loaderCallbacks.setSearch(searchQuery);
} }
@Override
public boolean onSearchRequested() {
// Hide trackController when search dialog is shown
trackController.hide();
return super.onSearchRequested();
}
@Override @Override
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() { protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return trackRecordingServiceConnection; return trackRecordingServiceConnection;
} }
@Override @Override
protected void onTrackDeleted() { protected void onDeleteConfirmed() {
// Do nothing // Do nothing
} }
@@ -425,22 +416,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
} }
} }
/**
* Updates the menu items with not fixed icon for gps option.
*
* @param isGpsStarted true if gps is started
* @param isRecording true if recording
*/
private void updateMenuItems(boolean isGpsStarted, boolean isRecording) {
updateGpsMenuItem(isGpsStarted, isRecording);
}
/** /**
* Updates the menu items with the icon specified. * Updates the menu items with the icon specified.
* *
* @param isGpsStarted true if gps is started * @param isGpsStarted true if gps is started
* @param isRecording true if recording * @param isRecording true if recording
*/ */
//TODO Check if if can be avoided to call this outside of onGpsStatusChanged()
private void updateGpsMenuItem(boolean isGpsStarted, boolean isRecording) { private void updateGpsMenuItem(boolean isGpsStarted, boolean isRecording) {
if (startGpsMenuItem != null) { if (startGpsMenuItem != null) {
startGpsMenuItem.setVisible(!isRecording); startGpsMenuItem.setVisible(!isRecording);
@@ -479,7 +461,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
return true; return true;
} }
if (itemId == R.id.list_context_menu_edit) { if (itemId == R.id.list_context_menu_edit) {
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class) Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]); .putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
@@ -491,6 +472,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
deleteTracks(trackIds); deleteTracks(trackIds);
return true; return true;
} }
if (itemId == R.id.list_context_menu_select_all) { if (itemId == R.id.list_context_menu_select_all) {
for (int i = 0; i < viewBinding.trackList.getCount(); i++) { for (int i = 0; i < viewBinding.trackList.getCount(); i++) {
viewBinding.trackList.setItemChecked(i, true); viewBinding.trackList.setItemChecked(i, true);
@@ -558,25 +540,25 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
public void recordStart() { public void recordStart() {
if (recordingStatus.getTrackId() == null) { if (recordingStatus.getTrackId() == null) {
// Not recording -> Recording // Not recording -> Recording
updateMenuItems(false, true); updateGpsMenuItem(false, true);
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class); Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
startActivity(newIntent); startActivity(newIntent);
} else if (recordingStatus.isPaused()) { } else if (recordingStatus.isPaused()) {
// Paused -> Resume // Paused -> Resume
updateMenuItems(false, true); updateGpsMenuItem(false, true);
trackRecordingServiceConnection.resumeTrack(); trackRecordingServiceConnection.resumeTrack();
} }
} }
@Override @Override
public void recordPause() { public void recordPause() {
updateMenuItems(false, true); updateGpsMenuItem(false, true);
trackRecordingServiceConnection.pauseTrack(); trackRecordingServiceConnection.pauseTrack();
} }
@Override @Override
public void recordStop() { public void recordStop() {
updateMenuItems(false, false); updateGpsMenuItem(false, false);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this); trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
} }
@@ -585,10 +567,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
updateGpsMenuItem(true, recordingStatus.isRecording()); updateGpsMenuItem(true, recordingStatus.isRecording());
} }
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) { private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status; recordingStatus = status;
trackController.update(recordingStatus);
} }
} }
@@ -42,6 +42,7 @@ import de.dennisguse.opentracks.fragments.ChartFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment; import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.IntervalsFragment; import de.dennisguse.opentracks.fragments.IntervalsFragment;
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment; import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
import de.dennisguse.opentracks.services.TrackDeleteService;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity; import de.dennisguse.opentracks.settings.SettingsActivity;
@@ -56,7 +57,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
//TODO Should not use TrackRecordingServiceConnection; only used to determine if there is NO current recording, to enable resume functionality. //TODO Should not use TrackRecordingServiceConnection; only used to determine if there is NO current recording, to enable resume functionality.
public class TrackRecordedActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface { public class TrackRecordedActivity extends AbstractTrackDeleteActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
private static final String TAG = TrackRecordedActivity.class.getSimpleName(); private static final String TAG = TrackRecordedActivity.class.getSimpleName();
@@ -248,9 +249,17 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
return recordingStatus.getTrackId(); return recordingStatus.getTrackId();
} }
@Override
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
super.onTrackDeleteStatus(deleteStatus);
Log.e("probando", "trackId: " + trackId);
if (deleteStatus.isDeleted(trackId)) {
runOnUiThread(this::finish);
}
}
@Override @Override
protected void onTrackDeleted() { protected void onDeleteConfirmed() {
runOnUiThread(this::finish); runOnUiThread(this::finish);
} }
@@ -43,7 +43,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
*/ */
//NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key. //NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key.
//This mode of communication should be removed anyhow. //This mode of communication should be removed anyhow.
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, TrackController.Callback { public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, ControllerFragment.Callback {
public static final String EXTRA_TRACK_ID = "track_id"; public static final String EXTRA_TRACK_ID = "track_id";
@@ -56,7 +56,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection; private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackDataHub trackDataHub; private TrackDataHub trackDataHub;
private TrackController trackController;
private TrackRecordingBinding viewBinding; private TrackRecordingBinding viewBinding;
@@ -90,8 +89,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
// A recording track is on. // A recording track is on.
trackDataHub.loadTrack(trackId); trackDataHub.loadTrack(trackId);
trackDataHub.setRecordingStatus(recordingStatus); trackDataHub.setRecordingStatus(recordingStatus);
trackController.onResume(recordingStatus);
} }
} }
}; };
@@ -148,8 +145,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
if (savedInstanceState != null) { if (savedInstanceState != null) {
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY)); viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
} }
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, false, this);
} }
@Override @Override
@@ -214,7 +209,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
//TODO Pass recordingStatus directly to them //TODO Pass recordingStatus directly to them
trackDataHub.loadTrack(trackId); trackDataHub.loadTrack(trackId);
trackDataHub.setRecordingStatus(recordingStatus); trackDataHub.setRecordingStatus(recordingStatus);
trackController.onResume(recordingStatus);
} }
/* /*
@@ -233,12 +227,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
outState.putParcelable(EXTRA_TRACK_ID, trackId); outState.putParcelable(EXTRA_TRACK_ID, trackId);
} }
@Override
protected void onPause() {
super.onPause();
trackController.onPause();
}
@Override @Override
protected void onStop() { protected void onStop() {
super.onStop(); super.onStop();
@@ -406,7 +394,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) { private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
recordingStatus = status; recordingStatus = status;
trackController.update(recordingStatus);
trackDataHub.setRecordingStatus(recordingStatus); trackDataHub.setRecordingStatus(recordingStatus);
setLockscreenPolicy(); setLockscreenPolicy();
@@ -40,8 +40,8 @@ import androidx.core.view.GestureDetectorCompat;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Optional;
import de.dennisguse.opentracks.MarkerDetailActivity; import de.dennisguse.opentracks.MarkerDetailActivity;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
@@ -424,7 +424,7 @@ public class ChartView extends View {
} }
} }
updateDimensions(); updateDimensions();
updatePaths(); updateSeries();
} }
} }
@@ -471,7 +471,7 @@ public class ChartView extends View {
private void zoomIn() { private void zoomIn() {
if (canZoomIn()) { if (canZoomIn()) {
zoomLevel++; zoomLevel++;
updatePaths(); updateSeries();
invalidate(); invalidate();
} }
} }
@@ -486,7 +486,7 @@ public class ChartView extends View {
scrollX = maxWidth; scrollX = maxWidth;
scrollTo(scrollX, 0); scrollTo(scrollX, 0);
} }
updatePaths(); updateSeries();
invalidate(); invalidate();
} }
} }
@@ -853,63 +853,49 @@ public class ChartView extends View {
} }
/** /**
* Updates paths.
* The path needs to be updated any time after the data or the dimensions change. * The path needs to be updated any time after the data or the dimensions change.
*/ */
private void updatePaths() { private void updateSeries() {
synchronized (chartPoints) { synchronized (chartPoints) {
for (ChartValueSeries chartValueSeries : seriesList) { seriesList.stream().forEach(this::updateSerie);
chartValueSeries.getPath().reset();
}
drawPaths();
closePaths();
} }
} }
private void drawPaths() { private void updateSerie(ChartValueSeries series) {
boolean[] hasMoved = new boolean[seriesList.size()]; final int yCorner = topBorder + effectiveHeight;
final Path path = series.getPath();
for (ChartPoint dataPoint : chartPoints) { boolean drawFirstPoint = false;
for (int i = 0; i < seriesList.size(); i++) { path.rewind();
ChartValueSeries chartValueSeries = seriesList.get(i);
if (!chartValueSeries.isChartPointValid(dataPoint)) { Iterator<ChartPoint> iterator = chartPoints.iterator();
continue; while (iterator.hasNext()) {
} ChartPoint point = iterator.next();
if (!series.isChartPointValid(point)) {
continue;
}
double value = chartValueSeries.extractDataFromChartPoint(dataPoint); double value = series.extractDataFromChartPoint(point);
Path path = chartValueSeries.getPath(); int x = getX(point.getTimeOrDistance());
int x = getX(dataPoint.getTimeOrDistance()); int y = getY(series, value);
int y = getY(chartValueSeries, value);
if (!hasMoved[i]) { // start from lower left corner
hasMoved[i] = true; if (!drawFirstPoint) {
path.moveTo(x, y); path.moveTo(x, yCorner);
} else { drawFirstPoint = true;
path.lineTo(x, y); }
}
// draw graph
path.lineTo(x, y);
// last point: move to lower right
if (!iterator.hasNext()) {
path.lineTo(x, yCorner);
} }
} }
}
private void closePaths() { // back to lower left corner
for (ChartValueSeries chartValueSeries : seriesList) { path.close();
Optional<ChartPoint> firstValid = chartPoints.stream().filter(chartValueSeries::isChartPointValid)
.findFirst();
Optional<ChartPoint> lastValid = chartPoints.stream().filter(chartValueSeries::isChartPointValid)
.reduce((first, second) -> second);
if (firstValid.isPresent() && lastValid.isPresent()) {
Path path = chartValueSeries.getPath();
int yCorner = topBorder + effectiveHeight;
path.lineTo(getX(lastValid.get().getTimeOrDistance()), yCorner);
path.lineTo(getX(firstValid.get().getTimeOrDistance()), yCorner);
double value = chartValueSeries.extractDataFromChartPoint(firstValid.get());
path.lineTo(getX(firstValid.get().getTimeOrDistance()), getY(chartValueSeries, value));
}
}
} }
/** /**
@@ -961,7 +947,7 @@ public class ChartView extends View {
width = newWidth; width = newWidth;
height = newHeight; height = newHeight;
updateEffectiveDimensions(); updateEffectiveDimensions();
updatePaths(); updateSeries();
} }
} }
@@ -155,7 +155,6 @@ public class TrackDataHub {
} }
started = false; started = false;
trackStatisticsUpdater = null;
//Unregister listeners //Unregister listeners
ContentResolver contentResolver = context.getContentResolver(); ContentResolver contentResolver = context.getContentResolver();
@@ -168,6 +167,7 @@ public class TrackDataHub {
handlerThread = null; handlerThread = null;
} }
handler = null; handler = null;
trackStatisticsUpdater = null;
} }
public void loadTrack(final @NonNull Track.Id trackId) { public void loadTrack(final @NonNull Track.Id trackId) {
@@ -24,6 +24,10 @@ public class Distance {
return of(distance_km * UnitConversions.KM_TO_M); return of(distance_km * UnitConversions.KM_TO_M);
} }
public static Distance ofMM(double distance_mm) {
return of(distance_mm * UnitConversions.MM_TO_M);
}
public static Distance one(boolean metricUnit) { public static Distance one(boolean metricUnit) {
if (metricUnit) { if (metricUnit) {
return Distance.ofKilometer(1); return Distance.ofKilometer(1);
@@ -61,7 +61,6 @@ import de.dennisguse.opentracks.util.UUIDUtils;
public class ContentProviderUtils { public class ContentProviderUtils {
private static final String TAG = ContentProviderUtils.class.getSimpleName(); private static final String TAG = ContentProviderUtils.class.getSimpleName();
private static final int MAX_LATITUDE = 90000000;
// The authority (the first part of the URI) for the app's content provider. // The authority (the first part of the URI) for the app's content provider.
@VisibleForTesting @VisibleForTesting
@@ -161,8 +160,10 @@ public class ContentProviderUtils {
@VisibleForTesting @VisibleForTesting
public void deleteAllTracks(Context context) { public void deleteAllTracks(Context context) {
//TODO Both calls should not be necessary
contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, null, null); contentResolver.delete(TrackPointsColumns.CONTENT_URI_BY_ID, null, null);
contentResolver.delete(MarkerColumns.CONTENT_URI, null, null); contentResolver.delete(MarkerColumns.CONTENT_URI, null, null);
// Delete tracks last since it triggers a database vaccum call // Delete tracks last since it triggers a database vaccum call
contentResolver.delete(TracksColumns.CONTENT_URI, null, null); contentResolver.delete(TracksColumns.CONTENT_URI, null, null);
@@ -190,6 +191,7 @@ public class ContentProviderUtils {
} }
//TODO Only use for tests; also move to tests. //TODO Only use for tests; also move to tests.
@VisibleForTesting
public List<Track> getTracks() { public List<Track> getTracks() {
ArrayList<Track> tracks = new ArrayList<>(); ArrayList<Track> tracks = new ArrayList<>();
try (Cursor cursor = getTrackCursor(null, null, TracksColumns._ID)) { try (Cursor cursor = getTrackCursor(null, null, TracksColumns._ID)) {
@@ -203,9 +205,6 @@ public class ContentProviderUtils {
return tracks; return tracks;
} }
/**
* @param trackId the track id.
*/
public Track getTrack(Track.Id trackId) { public Track getTrack(Track.Id trackId) {
if (trackId == null) { if (trackId == null) {
return null; return null;
@@ -218,9 +217,6 @@ public class ContentProviderUtils {
return null; return null;
} }
/**
* @param trackUUID the track uuid.
*/
public Track getTrack(@NonNull UUID trackUUID) { public Track getTrack(@NonNull UUID trackUUID) {
String trackUUIDsearch = UUIDUtils.toHex(trackUUID); String trackUUIDsearch = UUIDUtils.toHex(trackUUID);
try (Cursor cursor = getTrackCursor("hex(" + TracksColumns.UUID + ")=?", new String[]{trackUUIDsearch}, null)) { try (Cursor cursor = getTrackCursor("hex(" + TracksColumns.UUID + ")=?", new String[]{trackUUIDsearch}, null)) {
@@ -609,6 +605,7 @@ public class ContentProviderUtils {
* @param location the location * @param location the location
* @return trackPoint id if the location is in the track. -1L otherwise. * @return trackPoint id if the location is in the track. -1L otherwise.
*/ */
@Deprecated
public TrackPoint.Id getTrackPointId(Track.Id trackId, Location location) { public TrackPoint.Id getTrackPointId(Track.Id trackId, Location location) {
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TIME + "=?)"; String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TIME + "=?)";
String[] selectionArgs = new String[]{Long.toString(trackId.getId()), Long.toString(location.getTime())}; String[] selectionArgs = new String[]{Long.toString(trackId.getId()), Long.toString(location.getTime())};
@@ -740,6 +737,7 @@ public class ContentProviderUtils {
return new TrackPointIterator(this, trackId, startTrackPointId); return new TrackPointIterator(this, trackId, startTrackPointId);
} }
@Deprecated
private TrackPoint findTrackPointBy(String selection, String[] selectionArgs) { private TrackPoint findTrackPointBy(String selection, String[] selectionArgs) {
try (Cursor cursor = getTrackPointCursor(null, selection, selectionArgs, TrackPointsColumns._ID)) { try (Cursor cursor = getTrackPointCursor(null, selection, selectionArgs, TrackPointsColumns._ID)) {
if (cursor != null && cursor.moveToNext()) { if (cursor != null && cursor.moveToNext()) {
@@ -123,7 +123,7 @@ public final class SensorDataCycling {
return wheelRevolutionsTime; return wheelRevolutionsTime;
} }
public void compute(DistanceSpeed previous, int wheel_circumference_mm) { public void compute(DistanceSpeed previous, Distance wheelCircumference) {
if (hasData() && previous != null && previous.hasData()) { if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS; float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms); Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
@@ -134,7 +134,7 @@ public final class SensorDataCycling {
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX); long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
wheelDiff = Math.abs(wheelDiff); //HACK for Garmin Speed 2 as some of those seem to count backwards wheelDiff = Math.abs(wheelDiff); //HACK for Garmin Speed 2 as some of those seem to count backwards
Distance distance = Distance.of(wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M); Distance distance = wheelCircumference.multipliedBy(wheelDiff);
Distance distanceOverall = distance; Distance distanceOverall = distance;
if (previous.hasValue()) { if (previous.hasValue()) {
distanceOverall = distance.plus(previous.getValue().distanceOverall); distanceOverall = distance.plus(previous.getValue().distanceOverall);
@@ -518,13 +518,12 @@ public class KMLTrackExporter implements TrackExporter {
return location.getBearing(); return location.getBearing();
} }
private String getCoordinates(Location location, String separator) { private static String getCoordinates(Location location, String separator) {
StringBuilder builder = new StringBuilder(); String result = location.getLongitude() + separator + location.getLatitude();
builder.append(location.getLongitude()).append(separator).append(location.getLatitude());
if (location.hasAltitude()) { if (location.hasAltitude()) {
builder.append(separator).append(location.getAltitude()); result += separator + location.getAltitude();
} }
return builder.toString(); return result;
} }
/** /**
@@ -0,0 +1,171 @@
package de.dennisguse.opentracks.services;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
public class TrackDeleteService extends Service {
private static final String CHANNEL_ID = TrackDeleteService.class.getSimpleName();
private static final int NOTIFICATION_ID = 1;
static final String EXTRA_TRACK_IDS = "extra_track_ids";
private final Binder binder = new Binder();
private ExecutorService serviceExecutor;
private MutableLiveData<DeleteStatus> deleteResultObservable;
private NotificationManager notificationManager;
private NotificationCompat.Builder notificationBuilder;
@Override
public void onCreate() {
serviceExecutor = Executors.newSingleThreadExecutor();
deleteResultObservable = new MutableLiveData<>();
}
@Override
public void onDestroy() {
super.onDestroy();
if (serviceExecutor != null) {
serviceExecutor.shutdownNow();
serviceExecutor = null;
}
deleteResultObservable = null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
ArrayList<Track.Id> trackIds = intent.getParcelableArrayListExtra(EXTRA_TRACK_IDS);
createAndShowNotification(trackIds.size());
deleteTracks(trackIds);
return START_NOT_STICKY;
}
private void deleteTracks(@NonNull ArrayList<Track.Id> trackIds) {
sendResult(null,0, trackIds.size());
serviceExecutor.execute(() -> {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
for (int i = 0; i < trackIds.size(); i++) {
updateNotification(i + 1, trackIds.size());
contentProviderUtils.deleteTrack(this, trackIds.get(i));
sendResult(trackIds.get(i), i + 1, trackIds.size());
}
stopSelf();
});
}
private void sendResult(Track.Id trackId, int deletes, int total) {
if (deleteResultObservable != null) {
deleteResultObservable.postValue(new DeleteStatus(trackId, deletes, total));
}
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
public LiveData<DeleteStatus> getDeletingStatusObservable() {
return deleteResultObservable;
}
/**
* Starts and shows the notification.
*
* @param tracksToDelete number of tracks to be deleted.
*/
private void createAndShowNotification(int tracksToDelete) {
notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, this.getString(R.string.app_name), NotificationManager.IMPORTANCE_LOW);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
notificationChannel.setAllowBubbles(true);
}
notificationManager.createNotificationChannel(notificationChannel);
}
notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID);
notificationBuilder
.setContentTitle(this.getString(R.string.track_delete_progress_message))
.setContentText(this.getString(R.string.track_delete_progress, 0, tracksToDelete))
.setSmallIcon(R.drawable.ic_logo_color_24dp)
.setProgress(tracksToDelete, 0, false);
startForeground(NOTIFICATION_ID, notificationBuilder.build());
}
/**
* Updates notification progress.
*
* @param progress number of tracks already deleted.
* @param total total of tracks to be deleted.
*/
private void updateNotification(int progress, int total) {
notificationBuilder.setProgress(total, progress, false);
notificationBuilder.setContentText(this.getString(R.string.track_delete_progress, progress, total));
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
public class Binder extends android.os.Binder {
private Binder() {
super();
}
public TrackDeleteService getService() {
return TrackDeleteService.this;
}
}
public static class DeleteStatus {
private int progress;
private int max;
private Track.Id trackId;
/**
* @param trackId Track.Id just deleted if any.
* @param progress number of deletes.
* @param max total of deletes to be done.
*/
private DeleteStatus(@Nullable Track.Id trackId, int progress, int max) {
this.trackId = trackId;
this.progress = progress;
this.max = max;
}
public boolean isFinished() {
return progress == max;
}
public boolean isDeleted(Track.Id trackId) {
return this.trackId != null && this.trackId.equals(trackId);
}
@Override
public String toString() {
return "DeleteStatus{" +
"number of deletes=" + progress +
", total=" + max +
'}';
}
}
}
@@ -0,0 +1,70 @@
package de.dennisguse.opentracks.services;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.content.data.Track;
import static de.dennisguse.opentracks.services.TrackDeleteService.EXTRA_TRACK_IDS;
public class TrackDeleteServiceConnection implements ServiceConnection {
final private Listener listener;
private TrackDeleteService trackDeleteService;
public TrackDeleteServiceConnection(@NonNull Listener listener) {
this.listener = listener;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
trackDeleteService = ((TrackDeleteService.Binder) service).getService();
listener.connected();
}
@Override
public void onServiceDisconnected(ComponentName name) {
trackDeleteService = null;
}
public void startAndBind(Context context, ArrayList<Track.Id> trackIds) {
if (trackDeleteService != null) {
return;
}
Intent intent = new Intent(context, TrackDeleteService.class)
.putParcelableArrayListExtra(EXTRA_TRACK_IDS, trackIds);
context.startService(intent);
bind(context);
}
public void bind(Context context) {
if (trackDeleteService != null) {
return;
}
context.bindService(new Intent(context, TrackDeleteService.class), this, BuildConfig.DEBUG ? Context.BIND_DEBUG_UNBIND : 0);
}
public void unbind(Context context) {
context.unbindService(this);
trackDeleteService = null;
}
public TrackDeleteService getServiceIfBound() {
return trackDeleteService;
}
public interface Listener {
void connected();
}
}
@@ -25,6 +25,7 @@ import android.util.Log;
import java.time.Duration; import java.time.Duration;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.sensor.SensorData; import de.dennisguse.opentracks.content.sensor.SensorData;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling; import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.content.sensor.SensorDataSet;
@@ -57,7 +58,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
private boolean started = false; private boolean started = false;
private final SharedPreferences sharedPreferences; private final SharedPreferences sharedPreferences;
private int preferenceWheelCircumference; private Distance preferenceWheelCircumference;
private final BluetoothConnectionManager.HeartRate heartRate = new BluetoothConnectionManager.HeartRate(this); private final BluetoothConnectionManager.HeartRate heartRate = new BluetoothConnectionManager.HeartRate(this);
private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this); private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
@@ -138,11 +138,6 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
@Override @Override
public void run(@NonNull TrackRecordingService trackRecordingService) { public void run(@NonNull TrackRecordingService trackRecordingService) {
if (trackRecordingService == null) {
Log.e(TAG, "TrackRecordingService is null.");
return;
}
announce(trackRecordingService.getRecordingTrackId(), trackRecordingService.getTrackStatistics()); announce(trackRecordingService.getRecordingTrackId(), trackRecordingService.getTrackStatistics());
} }
@@ -3,10 +3,12 @@ package de.dennisguse.opentracks.settings;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.documentfile.provider.DocumentFile; import androidx.documentfile.provider.DocumentFile;
@@ -93,6 +95,9 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
public static class PrefsFragment extends PreferenceFragmentCompat { public static class PrefsFragment extends PreferenceFragmentCompat {
private TextToSpeech tts;
private int ttsInitStatus;
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> { private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
@@ -125,6 +130,22 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
// Used to forward update from ChooseActivityTypeDialogFragment; TODO Could be replaced with LiveData. // Used to forward update from ChooseActivityTypeDialogFragment; TODO Could be replaced with LiveData.
private ActivityTypePreference.ActivityPreferenceDialog activityPreferenceDialog; private ActivityTypePreference.ActivityPreferenceDialog activityPreferenceDialog;
private Preference voiceAnnouncements;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ttsInitStatus = TextToSpeech.ERROR;
tts = new TextToSpeech(getContext(), status -> {
ttsInitStatus = status;
Log.i(TAG, "TextToSpeech initialized with status " + status);
if (voiceAnnouncements != null) {
updateVoiceAnnouncements();
}
});
}
@Override @Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
sharedPreferences = PreferencesUtils.getSharedPreferences(getContext()); sharedPreferences = PreferencesUtils.getSharedPreferences(getContext());
@@ -150,6 +171,9 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
//Use same value for not set as Androidx ListPreference and EditTextPreference //Use same value for not set as Androidx ListPreference and EditTextPreference
return directory != null ? directory.getName() : getString(R.string.not_set); return directory != null ? directory.getName() : getString(R.string.not_set);
}); });
voiceAnnouncements = findPreference(getString(R.string.voice_frequency_key));
updateVoiceAnnouncements();
} }
@Override @Override
@@ -208,6 +232,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
super.onDestroy(); super.onDestroy();
trackRecordingServiceConnection.unbind(getContext()); trackRecordingServiceConnection.unbind(getContext());
sharedPreferences = null; sharedPreferences = null;
voiceAnnouncements = null;
tts = null;
} }
public void setDefaultActivity(String iconValue) { public void setDefaultActivity(String iconValue) {
@@ -296,6 +322,10 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
}); });
} }
private void updateVoiceAnnouncements() {
voiceAnnouncements.setEnabled(ttsInitStatus == TextToSpeech.SUCCESS);
}
private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) { private void onRecordingStatusChanged(TrackRecordingService.RecordingStatus status) {
this.recordingStatus = status; this.recordingStatus = status;
if (!status.isRecording() && isAdded()) { if (!status.isRecording() && isAdded()) {
@@ -303,5 +333,4 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
} }
} }
} }
} }
@@ -159,7 +159,7 @@ public class TrackStatisticsUpdater {
if (!trackPoint.hasSensorDistance()) { if (!trackPoint.hasSensorDistance()) {
// GPS-based distance/speed // GPS-based distance/speed
Distance movingDistance = lastMovingTrackPoint.distanceToPrevious(trackPoint); Distance movingDistance = trackPoint.distanceToPrevious(lastMovingTrackPoint);
if (movingDistance.lessThan(minGPSDistance) && !trackPoint.isMoving()) { if (movingDistance.lessThan(minGPSDistance) && !trackPoint.isMoving()) {
speedBuffer_mps.reset(); speedBuffer_mps.reset();
lastTrackPoint = trackPoint; lastTrackPoint = trackPoint;
@@ -23,7 +23,6 @@ import java.util.ArrayList;
import de.dennisguse.opentracks.ContextualActionModeCallback; import de.dennisguse.opentracks.ContextualActionModeCallback;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackController;
import static android.content.Context.VIBRATOR_SERVICE; import static android.content.Context.VIBRATOR_SERVICE;
@@ -88,7 +87,7 @@ public class ActivityUtils {
}); });
} }
public static void configureSearchWidget(Activity activity, final MenuItem menuItem, final TrackController trackController) { public static SearchView configureSearchWidget(Activity activity, final MenuItem menuItem) {
final SearchView searchView = (SearchView) menuItem.getActionView(); final SearchView searchView = (SearchView) menuItem.getActionView();
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE); SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
if (searchManager != null) { if (searchManager != null) {
@@ -97,47 +96,12 @@ public class ActivityUtils {
} else { } else {
Log.w(TAG, "Could not retrieve SearchManager."); Log.w(TAG, "Could not retrieve SearchManager.");
} }
searchView.setQueryRefinementEnabled(true);
searchView.setSubmitButtonEnabled(true); searchView.setSubmitButtonEnabled(true);
searchView.setOnQueryTextFocusChangeListener((v, hasFocus) -> { return searchView;
// Hide and show trackController when searchable widget has focus/no focus
if (trackController != null) {
if (hasFocus) {
trackController.hide();
} else {
trackController.show();
}
}
});
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
menuItem.collapseActionView();
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
return false;
}
});
searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() {
@Override
public boolean onSuggestionSelect(int position) {
return false;
}
@Override
public boolean onSuggestionClick(int position) {
menuItem.collapseActionView();
return false;
}
});
} }
public static void vibrate(Activity activity, int milliseconds) { public static void vibrate(Context context, int milliseconds) {
final Vibrator vibrator = (Vibrator) activity.getSystemService(VIBRATOR_SERVICE); final Vibrator vibrator = (Vibrator) context.getSystemService(VIBRATOR_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(milliseconds, VibrationEffect.DEFAULT_AMPLITUDE)); vibrator.vibrate(VibrationEffect.createOneShot(milliseconds, VibrationEffect.DEFAULT_AMPLITUDE));
} else { } else {
@@ -142,9 +142,9 @@ public class PreferencesUtils {
return getString(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_speed_key, getBluetoothSensorAddressNone(context)); return getString(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_speed_key, getBluetoothSensorAddressNone(context));
} }
public static int getWheelCircumference(SharedPreferences sharedPreferences, Context context) { public static Distance getWheelCircumference(SharedPreferences sharedPreferences, Context context) {
final int DEFAULT = Integer.parseInt(context.getResources().getString(R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_default)); final int DEFAULT = Integer.parseInt(context.getResources().getString(R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_default));
return getInt(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, DEFAULT); return Distance.ofMM(getInt(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, DEFAULT));
} }
public static String getBluetoothCyclingPowerSensorAddress(SharedPreferences sharedPreferences, Context context) { public static String getBluetoothCyclingPowerSensorAddress(SharedPreferences sharedPreferences, Context context) {
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/controller_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/controller_background"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<ImageButton
android:id="@+id/controller_record"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_record"
android:src="@drawable/ic_button_record" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="6">
<TextView
android:id="@+id/controller_total_time"
style="@style/TextSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="@android:color/white" />
<TextView
android:id="@+id/controller_status"
style="@style/TextMicro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/controller_total_time"
android:layout_centerHorizontal="true"
android:singleLine="true" />
</RelativeLayout>
<ImageButton
android:id="@+id/controller_stop"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_stop"
android:src="@drawable/button_stop" />
</LinearLayout>
@@ -1,69 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:id="@+id/track_controller_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/track_controller_background"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<ImageButton
android:id="@+id/track_controller_record"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_record"
android:src="@drawable/ic_button_record" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="6">
<TextView
android:id="@+id/track_controller_total_time"
style="@style/TextSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="@android:color/white" />
<TextView
android:id="@+id/track_controller_status"
style="@style/TextMicro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/track_controller_total_time"
android:layout_centerHorizontal="true"
android:singleLine="true" />
</RelativeLayout>
<ImageButton
android:id="@+id/track_controller_stop"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_stop"
android:src="@drawable/button_stop" />
</LinearLayout>
</merge>
+5 -3
View File
@@ -27,9 +27,11 @@ limitations under the License.
android:baselineAligned="false" android:baselineAligned="false"
android:orientation="horizontal"> android:orientation="horizontal">
<include <androidx.fragment.app.FragmentContainerView
android:id="@+id/track_controller_container" android:id="@+id/controller_fragment"
layout="@layout/track_controller" /> android:name="de.dennisguse.opentracks.ControllerFragment"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
<ListView <ListView
android:id="@+id/track_list" android:id="@+id/track_list"
+5 -3
View File
@@ -11,9 +11,11 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="horizontal"> android:orientation="horizontal">
<include <androidx.fragment.app.FragmentContainerView
android:id="@+id/track_controller_container" android:id="@+id/controller_fragment"
layout="@layout/track_controller" /> android:name="de.dennisguse.opentracks.ControllerFragment"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2012 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/controller_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/controller_background"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageButton
android:id="@+id/controller_record"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_record"
android:src="@drawable/ic_button_record" />
<RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="6">
<TextView
android:id="@+id/controller_total_time"
style="@style/TextSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:gravity="center"
android:textColor="@android:color/white"
tools:text="0:12:34" />
<TextView
android:id="@+id/controller_status"
style="@style/TextMicro"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/controller_total_time"
android:layout_alignParentStart="true"
android:gravity="center"
android:singleLine="true"
tools:text="@string/generic_recording" />
</RelativeLayout>
<ImageButton
android:id="@+id/controller_stop"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_stop"
android:src="@drawable/button_stop" />
</LinearLayout>
-75
View File
@@ -1,75 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2012 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:id="@+id/track_controller_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/track_controller_background"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageButton
android:id="@+id/track_controller_record"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_record"
android:src="@drawable/ic_button_record" />
<RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="6">
<TextView
android:id="@+id/track_controller_total_time"
style="@style/TextSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:gravity="center"
android:textColor="@android:color/white"
tools:text="0:12:34" />
<TextView
android:id="@+id/track_controller_status"
style="@style/TextMicro"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/track_controller_total_time"
android:layout_alignParentStart="true"
android:gravity="center"
android:singleLine="true"
tools:text="@string/generic_recording" />
</RelativeLayout>
<ImageButton
android:id="@+id/track_controller_stop"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_stop"
android:src="@drawable/button_stop" />
</LinearLayout>
</merge>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="100dp"
android:minWidth="100dp" />
</LinearLayout>
+5 -3
View File
@@ -83,7 +83,9 @@ limitations under the License.
</LinearLayout> </LinearLayout>
</RelativeLayout> </RelativeLayout>
<include <androidx.fragment.app.FragmentContainerView
android:id="@+id/track_controller_container" android:id="@+id/controller_fragment"
layout="@layout/track_controller" /> android:name="de.dennisguse.opentracks.ControllerFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>
+5 -3
View File
@@ -17,7 +17,9 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" /> android:layout_weight="1" />
<include <androidx.fragment.app.FragmentContainerView
android:id="@+id/track_controller_container" android:id="@+id/controller_fragment"
layout="@layout/track_controller" /> android:name="de.dennisguse.opentracks.ControllerFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>
+2 -2
View File
@@ -20,14 +20,14 @@ limitations under the License.
android:icon="@drawable/ic_statistics_24dp" android:icon="@drawable/ic_statistics_24dp"
android:title="@string/menu_aggregated_statistics" android:title="@string/menu_aggregated_statistics"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<!-- Icon and title is overwritten by TrackListActivity --> <!-- Icon, title, and visibility is managed by TrackListActivity -->
<!-- Hide icon as workaround for https://github.com/OpenTracksApp/OpenTracks/issues/200 -->
<item <item
android:id="@+id/track_list_start_gps" android:id="@+id/track_list_start_gps"
android:icon="@drawable/ic_gps_off_24dp" android:icon="@drawable/ic_gps_off_24dp"
android:title="@string/menu_start_gps" android:title="@string/menu_start_gps"
android:visible="false" android:visible="false"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<!-- TODO SearchAction is only working if it is shown as action. However, only two slots are available. See #757. -->
<item <item
android:id="@+id/track_list_search" android:id="@+id/track_list_search"
android:title="@string/menu_search" android:title="@string/menu_search"
+1
View File
@@ -611,4 +611,5 @@ Pokud zařízení GPS hlásí nepřesná data (např. Polohu, rychlost, nadmořs
<string name="track_distance_notification">Vzdálenost: %1$s</string> <string name="track_distance_notification">Vzdálenost: %1$s</string>
<string name="track_speed_notification">Rychlost: %1$s</string> <string name="track_speed_notification">Rychlost: %1$s</string>
<string name="track_pace_notification">Rychlost: %1$s</string> <string name="track_pace_notification">Rychlost: %1$s</string>
<string name="about_third_party_data">Údaje třetích stran</string>
</resources> </resources>
+26 -14
View File
@@ -124,7 +124,7 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
<string name="activity_type_sled">Schlittenfahren</string> <string name="activity_type_sled">Schlittenfahren</string>
<string name="activity_type_snow_boarding">Snowboarden</string> <string name="activity_type_snow_boarding">Snowboarden</string>
<string name="activity_type_snow_shoeing">Schneeschuhwandern</string> <string name="activity_type_snow_shoeing">Schneeschuhwandern</string>
<string name="activity_type_speed_walking">Speed-​​Walking</string> <string name="activity_type_speed_walking">Sportliches Gehen</string>
<string name="activity_type_street_running">Laufen (Straßenuntergrund)</string> <string name="activity_type_street_running">Laufen (Straßenuntergrund)</string>
<string name="activity_type_surfing">Surfen</string> <string name="activity_type_surfing">Surfen</string>
<string name="activity_type_track_cycling">Bahnradfahren</string> <string name="activity_type_track_cycling">Bahnradfahren</string>
@@ -214,9 +214,9 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
<string name="generic_tracks">Strecken</string> <string name="generic_tracks">Strecken</string>
<string name="generic_yes">Ja</string> <string name="generic_yes">Ja</string>
<!-- Gps --> <!-- Gps -->
<string name="gps_location_access">\"Standortzugriff\"</string> <string name="gps_location_access">„Standortzugriff“</string>
<string name="gps_disabled">GPS ist nicht aktiviert. Tippen Sie hier, um zu %1$s wechseln. Drücken Sie \"Zurück\", um zurückzugehen.</string> <string name="gps_disabled">GPS ist nicht aktiviert. Tippen Sie hier, um zu %1$s wechseln. Drücken Sie „Zurück“, um zurückzugehen.</string>
<string name="gps_disabled_my_location">Der aktuelle Standort kann nicht ermittelt werden. Aktivieren Sie mehr Standortquellen unter \"%1$s\" und vergewissern Sie sich, dass der Himmel sichtbar ist. Bitte versuchen Sie es später erneut.</string> <string name="gps_disabled_my_location">Der aktuelle Standort kann nicht ermittelt werden. Aktivieren Sie mehr Standortquellen unter „%1$s“ und vergewissern Sie sich, dass der Himmel sichtbar ist. Bitte versuchen Sie es später erneut.</string>
<string name="gps_not_found">Das Gerät empfängt kein GPS-Signal</string> <string name="gps_not_found">Das Gerät empfängt kein GPS-Signal</string>
<string name="gps_starting">GPS wird gestartet</string> <string name="gps_starting">GPS wird gestartet</string>
<string name="gps_wait_for_better_signal">Warten auf besseres GPS-Signal</string> <string name="gps_wait_for_better_signal">Warten auf besseres GPS-Signal</string>
@@ -231,7 +231,7 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
<string name="image_photo_marker">Fotomarkierung</string> <string name="image_photo_marker">Fotomarkierung</string>
<string name="image_record">Aufzeichnen</string> <string name="image_record">Aufzeichnen</string>
<string name="image_stop">Beenden</string> <string name="image_stop">Beenden</string>
<string name="image_track">Track</string> <string name="image_track">Strecke</string>
<!-- Import --> <!-- Import -->
<string name="import_error">%1$d von %2$s aus %3$s importiert</string> <string name="import_error">%1$d von %2$s aus %3$s importiert</string>
<string name="import_no_directory">%1$s ist nicht vorhanden.</string> <string name="import_no_directory">%1$s ist nicht vorhanden.</string>
@@ -292,7 +292,7 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
<!-- Search --> <!-- Search -->
<string name="search_global_description">Tracks und Markierungen</string> <string name="search_global_description">Tracks und Markierungen</string>
<string name="search_list_empty_message">Keine Tracks oder Markierungen gefunden, die Ihrer Suchanfrage entsprechen</string> <string name="search_list_empty_message">Keine Tracks oder Markierungen gefunden, die Ihrer Suchanfrage entsprechen</string>
<string name="search_list_marker_track_location">Im Track \"%1$s\".</string> <string name="search_list_marker_track_location">Im Track „%1$s“.</string>
<!-- Sensor State --> <!-- Sensor State -->
<string name="sensor_state_battery">Akkustatus</string> <string name="sensor_state_battery">Akkustatus</string>
<string name="sensor_state_cadence">Trittfrequenz</string> <string name="sensor_state_cadence">Trittfrequenz</string>
@@ -325,7 +325,7 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
<string name="settings_recording_auto_resume_track_timeout_summary">Eine Aufzeichnung nach dem Neustart fortsetzen, wenn der letzte Standort innerhalb der letzten %1$s aufgezeichnet wurde</string> <string name="settings_recording_auto_resume_track_timeout_summary">Eine Aufzeichnung nach dem Neustart fortsetzen, wenn der letzte Standort innerhalb der letzten %1$s aufgezeichnet wurde</string>
<string name="settings_recording_auto_resume_track_timeout_title">Automat. Fortsetzung</string> <string name="settings_recording_auto_resume_track_timeout_title">Automat. Fortsetzung</string>
<string name="settings_recording_default_activity_title">Standardaktivität</string> <string name="settings_recording_default_activity_title">Standardaktivität</string>
<string name="settings_recording_location_frequency_summary">Entfernungs-/Zeitintervall %1$s zwischen zwei Standortaufzeichnungen:</string> <string name="settings_recording_location_frequency_summary">Entfernungs-/Zeitintervall %1$s zwischen zwei Standortaufzeichnungen</string>
<string name="settings_recording_max_recording_distance_summary">Ein neues Segment wird erstellt, wenn die Entfernung zwischen zwei Standorten mehr als %1$s beträgt.</string> <string name="settings_recording_max_recording_distance_summary">Ein neues Segment wird erstellt, wenn die Entfernung zwischen zwei Standorten mehr als %1$s beträgt.</string>
<string name="settings_recording_max_recording_distance_title">Max. Entfernung</string> <string name="settings_recording_max_recording_distance_title">Max. Entfernung</string>
<string name="settings_recording_min_recording_distance_title">Entfernungsintervall aufz.</string> <string name="settings_recording_min_recording_distance_title">Entfernungsintervall aufz.</string>
@@ -379,7 +379,7 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
<string name="stats_distance">Entfernung</string> <string name="stats_distance">Entfernung</string>
<string name="stats_total_time">Gesamtzeit</string> <string name="stats_total_time">Gesamtzeit</string>
<!-- Track --> <!-- Track -->
<string name="track_name_format">Track %1$d</string> <string name="track_name_format">Strecke %1$d</string>
<string name="track_record_notification">Der Track wird aufgezeichnet…</string> <string name="track_record_notification">Der Track wird aufgezeichnet…</string>
<string name="track_recording_notification_accuracy">Standortgenauigkeit: %1$s</string> <string name="track_recording_notification_accuracy">Standortgenauigkeit: %1$s</string>
<string name="track_markers">Markierungen für %1$s</string> <string name="track_markers">Markierungen für %1$s</string>
@@ -491,7 +491,7 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
<string name="voice_total_distance_miles_other">Gesamtstrecke: %1$.2f Meilen</string> <string name="voice_total_distance_miles_other">Gesamtstrecke: %1$.2f Meilen</string>
<string name="voice_total_distance_miles_two">Gesamtstrecke: 2 Meilen</string> <string name="voice_total_distance_miles_two">Gesamtstrecke: 2 Meilen</string>
<string name="voice_total_distance_miles_zero">Gesamtstrecke: 0 Meilen</string> <string name="voice_total_distance_miles_zero">Gesamtstrecke: 0 Meilen</string>
<string name="voice_total_distance_zero">Die Gesamtstrecke in \"Meine Tracks\" ist null.</string> <string name="voice_total_distance_zero">Die Gesamtstrecke ist null.</string>
<!-- Waypoint Type --> <!-- Waypoint Type -->
<string name="waypoint_type_atm">Geldautomat</string> <string name="waypoint_type_atm">Geldautomat</string>
<string name="waypoint_type_bank">Bank</string> <string name="waypoint_type_bank">Bank</string>
@@ -534,7 +534,7 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
<string name="waypoint_type_shopping">Shopping</string> <string name="waypoint_type_shopping">Shopping</string>
<string name="waypoint_type_sight">Sehenswürdigkeit</string> <string name="waypoint_type_sight">Sehenswürdigkeit</string>
<string name="waypoint_type_store">Geschäft</string> <string name="waypoint_type_store">Geschäft</string>
<string name="waypoint_type_swimming_pool">Swimmingpool</string> <string name="waypoint_type_swimming_pool">Schwimmbad</string>
<string name="waypoint_type_telephone">Telefon</string> <string name="waypoint_type_telephone">Telefon</string>
<string name="waypoint_type_theater">Theater</string> <string name="waypoint_type_theater">Theater</string>
<string name="waypoint_type_toilet">Toilette</string> <string name="waypoint_type_toilet">Toilette</string>
@@ -557,9 +557,9 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
<string name="import_progress_summary_errors_msg">Fehler</string> <string name="import_progress_summary_errors_msg">Fehler</string>
<string name="import_progress_summary_exists_msg">Schon vorhanden</string> <string name="import_progress_summary_exists_msg">Schon vorhanden</string>
<string name="export_track_errors">Aufzeichnungen wurden nicht exportiert</string> <string name="export_track_errors">Aufzeichnungen wurden nicht exportiert</string>
<string name="settings_default_export_uri_title">Track Export Verzeichnis</string> <string name="settings_default_export_uri_title">Track-Export-Verzeichnis</string>
<string name="import_progress_summary_ok_msg">Dateien importiert</string> <string name="import_progress_summary_ok_msg">Dateien importiert</string>
<string name="export_all_do_it_for_all">Für aale Konflikte übernehmen</string> <string name="export_all_do_it_for_all">Für alle Konflikte übernehmen</string>
<string name="export_progress_summary_new_msg">Neue Tracks exportiert</string> <string name="export_progress_summary_new_msg">Neue Tracks exportiert</string>
<string name="activity_type_escooter">E-Scooter</string> <string name="activity_type_escooter">E-Scooter</string>
<string name="import_unsupported_format">Nicht unterstütztes Dateiformat</string> <string name="import_unsupported_format">Nicht unterstütztes Dateiformat</string>
@@ -578,7 +578,7 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
<string name="generic_overwrite">Überschreiben</string> <string name="generic_overwrite">Überschreiben</string>
<string name="generic_skip">Überspringen</string> <string name="generic_skip">Überspringen</string>
<string name="generic_separator_done_total">/</string> <string name="generic_separator_done_total">/</string>
<string name="generic_show_errors">Zeige Fehler</string> <string name="generic_show_errors">Fehler anzeigen</string>
<string name="generic_completed">Abgeschlossen</string> <string name="generic_completed">Abgeschlossen</string>
<string name="gps_disabled_msg">GPS deaktiviert</string> <string name="gps_disabled_msg">GPS deaktiviert</string>
<string name="export_progress_summary_overwrite_msg">Streckenaufzeichnung überschrieben</string> <string name="export_progress_summary_overwrite_msg">Streckenaufzeichnung überschrieben</string>
@@ -589,6 +589,18 @@ g) das tun, was Deiner Meinung nach OpenTracks hilft.</string>
<string name="voice_speed_lap">Rundengeschwindigkeit %1$s</string> <string name="voice_speed_lap">Rundengeschwindigkeit %1$s</string>
<string name="voice_pace_lap">Rundenzeit %1$s</string> <string name="voice_pace_lap">Rundenzeit %1$s</string>
<string name="toast_message_instant_track_was_exported">Letzte Aufzeichnung exportiert</string> <string name="toast_message_instant_track_was_exported">Letzte Aufzeichnung exportiert</string>
<string name="export_track_already_exists_msg">Der Track %1$s existiert bereits im Zielverzeichnis.</string> <string name="export_track_already_exists_msg">Die Streckenaufzeichung %1$s existiert bereits im Zielverzeichnis.</string>
<string name="gps_fixed_and_ready">GPS-Position bestimmt und bereit</string> <string name="gps_fixed_and_ready">GPS-Position bestimmt und bereit</string>
<string name="instant_export_enabled_title">Sofortiger Export im Anschluss an die Übung</string>
<string name="instant_export_enabled_summary">Streckenaufzeichnung nach Beendigung der Aufnahme in den Speicher exportieren</string>
<string name="generic_open_track">Offene Strecke</string>
<string name="sensor_state_power_avg">Durchschnittliche Leistung</string>
<string name="sensor_state_cadence_avg">Durchschnittliche Trittfrequenz</string>
<string name="sensor_state_cadence_max">Maximale Trittfrequenz</string>
<string name="stats_split_interval">Geteilte Intervalle</string>
<string name="track_distance_notification">Strecke: %1$s</string>
<string name="track_speed_notification">Geschwindigkeit: %1$s</string>
<string name="track_pace_notification">Tempo: %1$s</string>
<string name="value_pace_mile">%1$d:%2$02d min/mi</string>
<string name="value_pace_kilometer">%1$d:%2$02d min/km</string>
</resources> </resources>
+1
View File
@@ -611,4 +611,5 @@ Si el dispositivo GPS reporta datos inexactos (por ejemplo, ubicación, velocida
<string name="track_distance_notification">Distancia: %1$s</string> <string name="track_distance_notification">Distancia: %1$s</string>
<string name="track_speed_notification">Velocidad: %1$s</string> <string name="track_speed_notification">Velocidad: %1$s</string>
<string name="track_pace_notification">Ritmo: %1$s</string> <string name="track_pace_notification">Ritmo: %1$s</string>
<string name="about_third_party_data">Datos de terceros</string>
</resources> </resources>
+19 -19
View File
@@ -31,10 +31,10 @@ limitations under the License.
* toimii täysin ilman verkkoyhteyttä\n * toimii täysin ilman verkkoyhteyttä\n
* näytä jäljet kartalla (ks. alla)</string> * näytä jäljet kartalla (ks. alla)</string>
<string name="help_track_map_title">Kuinka näen jäljen kartalla?</string> <string name="help_track_map_title">Kuinka näen jäljen kartalla?</string>
<string name="help_track_map_content">" UsKäytä pientä karttakuvaketta. <string name="help_track_map_content"> Käytä pientä karttakuvaketta.
\n a) (live-päivityksiä) Asenna &lt;a href=\"https://github.com/OpenTracksApp/OSMDashboard\"&gt;OSMDashboard&lt;/a&gt;. Se tarjoaa online-tai offline-karttoja kautta &lt;a href=\"https://www.openstreetmap.org\"&gt;OpenStreetMap&lt;/a&gt;. \n a) (live-päivityksiä) Asenna <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Se tarjoaa online-tai offline-karttoja kautta <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
\n Itse asiassa, OSMDashboard on erillinen sovellus, joka vain voi käyttää (tilapäisesti) tiedot track(s), joka tulee näkyviin. \n OSMDashboard on erillinen sovellus, joka voi lukea väliaikaisesti reittien dataa näyttääkseen ne.
\n b) Kautta tavallinen kartta sovellus (esim. &lt;a href=\"http://osmand.net/\"&gt;OsmAnd&lt;/a&gt; ja &lt;a href=\"https://maps.me/\"&gt;KARTAT.MINULLE&lt;/a&gt;). Nämä sovellukset tarvitsevat tukea KMZ-muodossa (vain paikoissa ja aikaleimat). Itse asiassa sovelluksen on tuettava &lt; i&gt;ACTION_VIEW&lt; / i&gt; MIME &lt;i&gt;application / vnd: n kanssa.google-maa.kmz&lt;/i&gt;. "</string> \n b) Tavallisella karttasovelluksella (esim. <a href="http://osmand.net/">OsmAnd</a> ja <a href="https://maps.me/">MAPS.ME</a>). Näiden sovellusten pitää tukea reittejä KMZ-muodossa (vain paikat ja aikaleimat). Itse asiassa sovelluksen on tuettava <i>ACTION_VIEW</i> MIME:llä <i>application/vnd.google-earth.kmz</i>. </string>
<string name="help_storage_pictures_title">Miten OpenTracks tallentaa merkkeihin liittyen otetut kuvat?</string> <string name="help_storage_pictures_title">Miten OpenTracks tallentaa merkkeihin liittyen otetut kuvat?</string>
<string name="help_storage_pictures_content">Ne säilytetään sovelluksen sisäisessä tallennustilassa (eli ne näkyvät vain OpenTracksin sisällä).</string> <string name="help_storage_pictures_content">Ne säilytetään sovelluksen sisäisessä tallennustilassa (eli ne näkyvät vain OpenTracksin sisällä).</string>
<string name="help_track_export_title">Miten vien tai jaan tallennettuja reittejä?</string> <string name="help_track_export_title">Miten vien tai jaan tallennettuja reittejä?</string>
@@ -56,13 +56,13 @@ limitations under the License.
\nJos kaikki ei auta, ilmoita virheestä. </string> \nJos kaikki ei auta, ilmoita virheestä. </string>
<string name="help_support_title">Miten voin tukea OpenTracksin kehitystä?</string> <string name="help_support_title">Miten voin tukea OpenTracksin kehitystä?</string>
<string name="help_support_content"> Sinä pystyt: <string name="help_support_content"> Sinä pystyt:
\n \u0020a) levittää sanaa OpenTracksista, \na) levittää sanaa OpenTracksista,
\n \u0020b) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> ehdottaa ominaisuuksia </a>, \nb) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> ehdottaa ominaisuuksia </a>,
\n \u0020c) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> ilmoita virheistä </a>, \nc) <a href="https://github.com/OpenTracksApp/OpenTracks/issues"> ilmoita virheistä </a>,
\n \u0020d) auttaa ohjelmistokehityksessä, \nd) auttaa ohjelmistokehityksessä,
\n \u0020e) osta <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore"> Google Play -kaupasta </a>, \ne) osta <a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore"> Google Play -kaupasta </a>,
\n \u0020f) lahjoittaa ja \nf) lahjoittaa ja
\n \u0020g) tee mitä tahansa mielestäsi auttaa OpenTracksia. </string> \ng) tee mitä tahansa mielestäsi auttaa OpenTracksia. </string>
<string name="open_track_using_dashboard">Avaa OpenTracks-Dashboardilla</string> <string name="open_track_using_dashboard">Avaa OpenTracks-Dashboardilla</string>
<string name="open_track_as_KMZ">Avaa KMZ-muodossa</string> <string name="open_track_as_KMZ">Avaa KMZ-muodossa</string>
<string name="open_track_as_GPX">Avaa GPX-muodossa</string> <string name="open_track_as_GPX">Avaa GPX-muodossa</string>
@@ -575,14 +575,14 @@ jotta ne voidaan tuoda kohteeseen %1$s.</string>
<string name="import_progress_summary_ok_msg">Tuodut tiedostot</string> <string name="import_progress_summary_ok_msg">Tuodut tiedostot</string>
<string name="import_progress_summary_exists_msg">Jo olemassa</string> <string name="import_progress_summary_exists_msg">Jo olemassa</string>
<string name="import_progress_summary_errors_msg">Virhe</string> <string name="import_progress_summary_errors_msg">Virhe</string>
<string name="generic_click_twice_cancel">Peruuta uudelleen napsauttamalla</string> <string name="generic_click_twice_cancel">Paina takaisin uudestaan keskeyttääksesi</string>
<string name="settings_recording_fullscreen_on_while_recording_title">Kokoruutu</string> <string name="settings_recording_fullscreen_on_while_recording_title">Kokoruutu</string>
<string name="interval_list_empty_message">Ei ole mitään intervallia vielä</string> <string name="interval_list_empty_message">Ei ole mitään intervallia vielä</string>
<string name="export_error_post_workout">Treenin jälkeinen vienti epäonnistui, tarkista vientihakemisto.</string> <string name="export_error_post_workout">Treenin jälkeinen vienti epäonnistui, tarkista vientihakemisto.</string>
<string name="export_progress_summary_overwrite_msg">Kappaleet ylikirjoitettu</string> <string name="export_progress_summary_overwrite_msg">Kappaleet ylikirjoitettu</string>
<plurals name="generic_completed_with_errors"> <plurals name="generic_completed_with_errors">
<item quantity="one">Täydennetty yhdellä virheellä</item> <item quantity="one">Valmista yhdellä virheellä</item>
<item quantity="other">Täydennetty %1$d virheillä</item> <item quantity="other">Valmista %1$d virheellä</item>
</plurals> </plurals>
<string name="export_track_already_exists_msg">Kappale %1$s on jo olemassa kohdehakemistossa.</string> <string name="export_track_already_exists_msg">Kappale %1$s on jo olemassa kohdehakemistossa.</string>
<string name="export_all_do_it_for_all">tee se kaikkien konfliktien vuoksi</string> <string name="export_all_do_it_for_all">tee se kaikkien konfliktien vuoksi</string>
@@ -595,12 +595,12 @@ jotta ne voidaan tuoda kohteeseen %1$s.</string>
<string name="export_progress_summary_new_msg">Uusia kappaleita viety</string> <string name="export_progress_summary_new_msg">Uusia kappaleita viety</string>
<string name="export_progress_summary_skip_msg">Kappaleet ohitettu</string> <string name="export_progress_summary_skip_msg">Kappaleet ohitettu</string>
<string name="export_progress_summary_errors_msg">Virhe</string> <string name="export_progress_summary_errors_msg">Virhe</string>
<string name="generic_overwrite">Korvata</string> <string name="generic_overwrite">Korvaa</string>
<string name="generic_skip">Ohittaa</string> <string name="generic_skip">Ohita</string>
<string name="generic_separator_done_total">/</string> <string name="generic_separator_done_total">/</string>
<string name="generic_show_errors">Näytä Virheet</string> <string name="generic_show_errors">Näytä virheet</string>
<string name="gps_disabled_msg">GPS pois käytöstä</string> <string name="gps_disabled_msg">GPS pois käytöstä</string>
<string name="generic_completed">Täydellinen</string> <string name="generic_completed">Valmis</string>
<string name="sensor_state_power_avg">Avg Teho</string> <string name="sensor_state_power_avg">Avg Teho</string>
<string name="description_altitude_loss">Korkeussijaintitappio: %1$d m (%2$d ft)</string> <string name="description_altitude_loss">Korkeussijaintitappio: %1$d m (%2$d ft)</string>
<string name="sensor_state_heart_rate_max">Maksimisyke</string> <string name="sensor_state_heart_rate_max">Maksimisyke</string>
@@ -611,7 +611,7 @@ jotta ne voidaan tuoda kohteeseen %1$s.</string>
<string name="sensor_state_cadence_avg">Keskim.Poljinnopeus</string> <string name="sensor_state_cadence_avg">Keskim.Poljinnopeus</string>
<string name="sensor_state_cadence_max">Suurin Poljinnopeus</string> <string name="sensor_state_cadence_max">Suurin Poljinnopeus</string>
<string name="value_float_mile_hour">%1$.2f mt</string> <string name="value_float_mile_hour">%1$.2f mt</string>
<string name="generic_open_track">Avaa Raita</string> <string name="generic_open_track">Avattu reitti</string>
<string name="track_distance_notification">Etäisyys: %1$s</string> <string name="track_distance_notification">Etäisyys: %1$s</string>
<string name="track_pace_notification">Vauhti: %1$s</string> <string name="track_pace_notification">Vauhti: %1$s</string>
<string name="track_speed_notification">Nopeus: %1$s</string> <string name="track_speed_notification">Nopeus: %1$s</string>
+5 -5
View File
@@ -13,8 +13,7 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> --><resources xmlns:tools="http://schemas.android.com/tools">
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="about_preference_title">À propos d\'OpenTracks</string> <string name="about_preference_title">À propos d\'OpenTracks</string>
<string name="about_description"><i>OpenTracks</i> est une application de suivi sportif qui respecte totalement votre vie privée.</string> <string name="about_description"><i>OpenTracks</i> est une application de suivi sportif qui respecte totalement votre vie privée.</string>
<string name="about_url">Page web du projet : <a href="%1$s">%1$s</a></string> <string name="about_url">Page web du projet : <a href="%1$s">%1$s</a></string>
@@ -206,7 +205,7 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
<string name="generic_tracks">Parcours</string> <string name="generic_tracks">Parcours</string>
<string name="generic_yes">Oui</string> <string name="generic_yes">Oui</string>
<!-- Gps --> <!-- Gps -->
<string name="gps_location_access">\"Données de localisation\"</string> <string name="gps_location_access">« Données de localisation »</string>
<string name="gps_disabled">Le GPS n\'est pas activé. Appuyez ici pour accéder aux %1$s. Appuyez sur le bouton Retour pour revenir en arrière.</string> <string name="gps_disabled">Le GPS n\'est pas activé. Appuyez ici pour accéder aux %1$s. Appuyez sur le bouton Retour pour revenir en arrière.</string>
<string name="gps_disabled_my_location">Impossible de déterminer la position actuelle. Veuillez activer d\'autres sources de localisation dans les %1$s et vous assurer <string name="gps_disabled_my_location">Impossible de déterminer la position actuelle. Veuillez activer d\'autres sources de localisation dans les %1$s et vous assurer
que le ciel est visible. Veuillez réessayer ultérieurement.</string> que le ciel est visible. Veuillez réessayer ultérieurement.</string>
@@ -285,7 +284,7 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
<!-- Search --> <!-- Search -->
<string name="search_global_description">Parcours et repères</string> <string name="search_global_description">Parcours et repères</string>
<string name="search_list_empty_message">Aucun parcours ni repère ne correspond à votre recherche</string> <string name="search_list_empty_message">Aucun parcours ni repère ne correspond à votre recherche</string>
<string name="search_list_marker_track_location">Dans la piste \" %1$s \".</string> <string name="search_list_marker_track_location">Dans la piste « %1$s ».</string>
<!-- Sensor State --> <!-- Sensor State -->
<string name="sensor_state_battery">Niveau de la batterie</string> <string name="sensor_state_battery">Niveau de la batterie</string>
<string name="sensor_state_cadence">Cadences</string> <string name="sensor_state_cadence">Cadences</string>
@@ -592,7 +591,7 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
<string name="generic_overwrite">Écraser</string> <string name="generic_overwrite">Écraser</string>
<string name="generic_show_errors">Afficher les erreurs</string> <string name="generic_show_errors">Afficher les erreurs</string>
<string name="generic_separator_done_total">/</string> <string name="generic_separator_done_total">/</string>
<string name="generic_skip">Sauter</string> <string name="generic_skip">Ignorer</string>
<string name="generic_open_track">Voie ouverte</string> <string name="generic_open_track">Voie ouverte</string>
<string name="gps_disabled_msg">GPS désactivé</string> <string name="gps_disabled_msg">GPS désactivé</string>
<string name="gps_fixed_and_ready">GPS fixe et prêt</string> <string name="gps_fixed_and_ready">GPS fixe et prêt</string>
@@ -612,4 +611,5 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
<string name="description_altitude_loss">Perte d\'altitude : %1$d m (%2$d ft)</string> <string name="description_altitude_loss">Perte d\'altitude : %1$d m (%2$d ft)</string>
<string name="export_track_already_exists_msg">La piste %1$s existe déjà dans le répertoire de destination.</string> <string name="export_track_already_exists_msg">La piste %1$s existe déjà dans le répertoire de destination.</string>
<string name="export_progress_summary_overwrite_msg">Pistes écrasées</string> <string name="export_progress_summary_overwrite_msg">Pistes écrasées</string>
<string name="about_third_party_data">Données de tiers</string>
</resources> </resources>
+1
View File
@@ -616,4 +616,5 @@ Se te detés ou estás en interiores, non se gardarán datos do sensor (mais ser
<string name="track_distance_notification">Distancia: %1$s</string> <string name="track_distance_notification">Distancia: %1$s</string>
<string name="track_pace_notification">Ritmo: %1$s</string> <string name="track_pace_notification">Ritmo: %1$s</string>
<string name="track_speed_notification">Velocidade: %1$s</string> <string name="track_speed_notification">Velocidade: %1$s</string>
<string name="about_third_party_data">Datos de terceiros alleos</string>
</resources> </resources>
+1
View File
@@ -605,4 +605,5 @@ limitations under the License.
\n * jelölések létrehozása (képekkel együtt) \n * jelölések létrehozása (képekkel együtt)
\n * teljesen offline működik \n * teljesen offline működik
\n * pályák megjelenítése térképen (lásd alább)</string> \n * pályák megjelenítése térképen (lásd alább)</string>
<string name="about_third_party_data">Harmadik féltől származó adatok</string>
</resources> </resources>
+7 -6
View File
@@ -196,7 +196,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="generic_tracks">Percorsi</string> <string name="generic_tracks">Percorsi</string>
<string name="generic_yes">Sì</string> <string name="generic_yes">Sì</string>
<!-- Gps --> <!-- Gps -->
<string name="gps_location_access">\"Accesso alla posizione\"</string> <string name="gps_location_access">«Accesso alla posizione»</string>
<string name="gps_disabled">GPS non attivato. Tocca qui per andare a %1$s. Poi premi il pulsante per tornare indietro.</string> <string name="gps_disabled">GPS non attivato. Tocca qui per andare a %1$s. Poi premi il pulsante per tornare indietro.</string>
<string name="gps_disabled_my_location">Impossibile determinare la posizione corrente. Attiva ulteriori fonti per la determinazione della posizione in %1$s e assicurati che <string name="gps_disabled_my_location">Impossibile determinare la posizione corrente. Attiva ulteriori fonti per la determinazione della posizione in %1$s e assicurati che
il cielo sia visibile. Riprova più tardi.</string> il cielo sia visibile. Riprova più tardi.</string>
@@ -271,7 +271,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<!-- Search --> <!-- Search -->
<string name="search_global_description">Percorsi e indicatori</string> <string name="search_global_description">Percorsi e indicatori</string>
<string name="search_list_empty_message">Non ci sono percorsi o indicatori corrispondenti alla tua ricerca</string> <string name="search_list_empty_message">Non ci sono percorsi o indicatori corrispondenti alla tua ricerca</string>
<string name="search_list_marker_track_location">Nel percorso \"%1$s\".</string> <string name="search_list_marker_track_location">Nel percorso «%1$s».</string>
<!-- Sensor State --> <!-- Sensor State -->
<string name="sensor_state_battery">Livello batteria</string> <string name="sensor_state_battery">Livello batteria</string>
<string name="sensor_state_cadence">Cadenza</string> <string name="sensor_state_cadence">Cadenza</string>
@@ -553,7 +553,7 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="export_all_do_it_for_all">Esegui ciò per tutti i conflitti</string> <string name="export_all_do_it_for_all">Esegui ciò per tutti i conflitti</string>
<string name="export_track_errors">Percorsi non esportati</string> <string name="export_track_errors">Percorsi non esportati</string>
<string name="generic_overwrite">Sovrascrivere</string> <string name="generic_overwrite">Sovrascrivere</string>
<string name="generic_skip">Saltare</string> <string name="generic_skip">Ignora</string>
<string name="generic_separator_done_total">/</string> <string name="generic_separator_done_total">/</string>
<string name="generic_show_errors">Mostra gli errori</string> <string name="generic_show_errors">Mostra gli errori</string>
<string name="generic_completed">Completato</string> <string name="generic_completed">Completato</string>
@@ -589,9 +589,9 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="share_image_body">Penso che potresti essere interessat@ in questa immagine.</string> <string name="share_image_body">Penso che potresti essere interessat@ in questa immagine.</string>
<string name="hold_to_stop">Tieni premuto per fermare</string> <string name="hold_to_stop">Tieni premuto per fermare</string>
<string name="hold_to_pause">Tieni premuto per mettere in pausa</string> <string name="hold_to_pause">Tieni premuto per mettere in pausa</string>
<string name="instant_export_enabled_summary">È stata completata l\' esportazione del percorso finito nella memoria interna del tuo dispositivo</string> <string name="instant_export_enabled_summary">È stata completata l\'esportazione del percorso finito nella memoria interna del tuo dispositivo</string>
<string name="instant_export_enabled_title">Esportazione istantanea post-allenamento</string> <string name="instant_export_enabled_title">Esportazione istantanea post-allenamento</string>
<string name="generic_open_track">Apri traccia</string> <string name="generic_open_track">Traccia aperta</string>
<string name="import_prevent_reimport">È stato impostato impedisci reimportazione traccia</string> <string name="import_prevent_reimport">È stato impostato impedisci reimportazione traccia</string>
<string name="export_error_post_workout">Esportazione post-allenamento non riuscita, controllare la directory di esportazione.</string> <string name="export_error_post_workout">Esportazione post-allenamento non riuscita, controllare la directory di esportazione.</string>
<string name="settings_prevent_reimport_tracks_title">Impedire la reimportazione delle tracce</string> <string name="settings_prevent_reimport_tracks_title">Impedire la reimportazione delle tracce</string>
@@ -618,4 +618,5 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="track_distance_notification">Distanza: %1$s</string> <string name="track_distance_notification">Distanza: %1$s</string>
<string name="track_speed_notification">Velocità: %1$s</string> <string name="track_speed_notification">Velocità: %1$s</string>
<string name="track_pace_notification">Ritmo: %1$s</string> <string name="track_pace_notification">Ritmo: %1$s</string>
</resources> <string name="about_third_party_data">Dati di terzi</string>
</resources>
+2 -1
View File
@@ -24,7 +24,7 @@ limitations under the License.
<string name="activity_type_commercial_airplane">passasjerfly</string> <string name="activity_type_commercial_airplane">passasjerfly</string>
<string name="activity_type_cross_country_skiing">langrenn</string> <string name="activity_type_cross_country_skiing">langrenn</string>
<string name="activity_type_cycling">sykling</string> <string name="activity_type_cycling">sykling</string>
<string name="activity_type_dirt_bike">dirt bike</string> <string name="activity_type_dirt_bike">motocross</string>
<string name="activity_type_donkey_back_riding">eselridning</string> <string name="activity_type_donkey_back_riding">eselridning</string>
<string name="activity_type_driving">kjøring</string> <string name="activity_type_driving">kjøring</string>
<string name="activity_type_driving_bus">busskjøring</string> <string name="activity_type_driving_bus">busskjøring</string>
@@ -606,4 +606,5 @@ limitations under the License.
<string name="track_distance_notification">Distanse: %1$s</string> <string name="track_distance_notification">Distanse: %1$s</string>
<string name="track_speed_notification">Hastighet: %1$s</string> <string name="track_speed_notification">Hastighet: %1$s</string>
<string name="track_pace_notification">Tempo: %1$s</string> <string name="track_pace_notification">Tempo: %1$s</string>
<string name="about_third_party_data">Tredjeparts data</string>
</resources> </resources>
+1
View File
@@ -610,4 +610,5 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
<string name="track_speed_notification">Snelheid: %1$s</string> <string name="track_speed_notification">Snelheid: %1$s</string>
<string name="track_distance_notification">Afstand: %1$s</string> <string name="track_distance_notification">Afstand: %1$s</string>
<string name="track_pace_notification">Tempo: %1$s</string> <string name="track_pace_notification">Tempo: %1$s</string>
<string name="about_third_party_data">Gegevens van derden</string>
</resources> </resources>
+1
View File
@@ -613,4 +613,5 @@ limitations under the License.
<string name="value_pace_mile">%1$d:%2$02d min/mi</string> <string name="value_pace_mile">%1$d:%2$02d min/mi</string>
<string name="value_float_mile_hour">%1$.2f mph</string> <string name="value_float_mile_hour">%1$.2f mph</string>
<string name="value_float_kilometer_hour">%1$.2f km/h</string> <string name="value_float_kilometer_hour">%1$.2f km/h</string>
<string name="about_third_party_data">Dane osób trzecich</string>
</resources> </resources>
+4 -3
View File
@@ -149,7 +149,7 @@
<string name="image_record">Gravar</string> <string name="image_record">Gravar</string>
<string name="image_stop">Parar</string> <string name="image_stop">Parar</string>
<string name="image_track">Rota</string> <string name="image_track">Rota</string>
<string name="import_error">Importação: %1$d de %2$s de %3$s</string> <string name="import_error">Importado %1$d a %2$s de %3$s</string>
<string name="import_no_directory">%1$s não existe</string> <string name="import_no_directory">%1$s não existe</string>
<string name="import_no_file">Nenhum arquivo para importar. Conecte o dispositivo ao computador e coloque os <string name="import_no_file">Nenhum arquivo para importar. Conecte o dispositivo ao computador e coloque os
arquivos para importação em %1$s.</string> arquivos para importação em %1$s.</string>
@@ -200,10 +200,10 @@
<string name="search_list_marker_track_location">Na trilha \"%1$s\".</string> <string name="search_list_marker_track_location">Na trilha \"%1$s\".</string>
<string name="sensor_state_battery">Nível da bateria</string> <string name="sensor_state_battery">Nível da bateria</string>
<string name="sensor_state_cadence">Cadência</string> <string name="sensor_state_cadence">Cadência</string>
<string name="sensor_state_cadence_value">%1$d rpm</string> <string name="sensor_state_cadence_value">%1$d ram</string>
<string name="sensor_state_disconnected">Desconectado</string> <string name="sensor_state_disconnected">Desconectado</string>
<string name="sensor_state_heart_rate">Frequência cardíaca</string> <string name="sensor_state_heart_rate">Frequência cardíaca</string>
<string name="sensor_state_heart_rate_value">%1$d bpm</string> <string name="sensor_state_heart_rate_value">%1$d bom</string>
<string name="sensor_state_power">Alimentação</string> <string name="sensor_state_power">Alimentação</string>
<string name="sensor_state_power_value">%1$d W</string> <string name="sensor_state_power_value">%1$d W</string>
<string name="settings_not_while_recording">Não disponível durante a gravação de uma trilha</string> <string name="settings_not_while_recording">Não disponível durante a gravação de uma trilha</string>
@@ -557,4 +557,5 @@
<string name="track_distance_notification">Distância: %1$s</string> <string name="track_distance_notification">Distância: %1$s</string>
<string name="track_speed_notification">Velocidade: %1$s</string> <string name="track_speed_notification">Velocidade: %1$s</string>
<string name="track_pace_notification">Parcela: %1$s</string> <string name="track_pace_notification">Parcela: %1$s</string>
<string name="about_third_party_data">Dados de terceiros</string>
</resources> </resources>
+1
View File
@@ -607,4 +607,5 @@ limitations under the License.
<string name="track_distance_notification">Відстань: %1$s</string> <string name="track_distance_notification">Відстань: %1$s</string>
<string name="track_speed_notification">Швидкість: %1$s</string> <string name="track_speed_notification">Швидкість: %1$s</string>
<string name="track_pace_notification">Темп: %1$s</string> <string name="track_pace_notification">Темп: %1$s</string>
<string name="about_third_party_data">Дані третіх сторін</string>
</resources> </resources>
+21 -4
View File
@@ -231,10 +231,10 @@ limitations under the License.
<!-- Sensor State --> <!-- Sensor State -->
<string name="sensor_state_battery">Mức pin</string> <string name="sensor_state_battery">Mức pin</string>
<string name="sensor_state_cadence">Nhịp</string> <string name="sensor_state_cadence">Nhịp</string>
<string name="sensor_state_cadence_value">%1$d rpm</string> <string name="sensor_state_cadence_value">%1$d vòng/phút</string>
<string name="sensor_state_disconnected">Đã ngắt kết nối</string> <string name="sensor_state_disconnected">Đã ngắt kết nối</string>
<string name="sensor_state_heart_rate">Nhịp tim</string> <string name="sensor_state_heart_rate">Nhịp tim</string>
<string name="sensor_state_heart_rate_value">%1$d bpm</string> <string name="sensor_state_heart_rate_value">%1$d nhịp đập/phút</string>
<string name="sensor_state_power">Công suất</string> <string name="sensor_state_power">Công suất</string>
<string name="sensor_state_power_value">%1$d W</string> <string name="sensor_state_power_value">%1$d W</string>
<string name="settings_not_while_recording">Không khả dụng khi đang ghi tuyến đường</string> <string name="settings_not_while_recording">Không khả dụng khi đang ghi tuyến đường</string>
@@ -279,7 +279,9 @@ limitations under the License.
<string name="settings_stats_units_title">Đơn vị ưa thích</string> <string name="settings_stats_units_title">Đơn vị ưa thích</string>
<!-- Share Track --> <!-- Share Track -->
<string name="share_track_share_file">Chia sẻ tệp tuyến đường</string> <string name="share_track_share_file">Chia sẻ tệp tuyến đường</string>
<string name="share_track_share_file_body">Tôi nghĩ rằng bạn có thể quan tâm đến tuyến đường này.\n\nĐể xem tuyến đường này, hãy sử dụng Google Earth hoặc phần mềm vẽ bản đồ khác.\n\n%1$s</string> <string name="share_track_share_file_body">Tôi nghĩ bạn có thể sẽ quan tâm đến tuyến đường này.
\n
\n%1$s</string>
<string name="share_track_subject">Tôi muốn chia sẻ một tuyến đường với bạn</string> <string name="share_track_subject">Tôi muốn chia sẻ một tuyến đường với bạn</string>
<!-- Stats --> <!-- Stats -->
<string name="stats_average_moving_pace">Nhịp độ di chuyển trung bình</string> <string name="stats_average_moving_pace">Nhịp độ di chuyển trung bình</string>
@@ -306,7 +308,7 @@ limitations under the License.
<string name="track_markers">%1$s Điểm đánh dấu</string> <string name="track_markers">%1$s Điểm đánh dấu</string>
<!-- Track Delete --> <!-- Track Delete -->
<string name="track_delete_one_confirm_message">Tuyến đường và các điểm đánh dấu của nó sẽ bị xóa vĩnh viễn khỏi thiết bị.</string> <string name="track_delete_one_confirm_message">Tuyến đường và các điểm đánh dấu của nó sẽ bị xóa vĩnh viễn khỏi thiết bị.</string>
<string name="track_delete_one_confirm_title">Xóa tuyến đường</string> <string name="track_delete_one_confirm_title">Xóa tuyến đường\?</string>
<string name="track_delete_multiple_confirm_message">Các tuyến đường được chọn và điểm đánh dấu của chúng sẽ bị xóa vĩnh viễn khỏi thiết bị.</string> <string name="track_delete_multiple_confirm_message">Các tuyến đường được chọn và điểm đánh dấu của chúng sẽ bị xóa vĩnh viễn khỏi thiết bị.</string>
<string name="track_delete_progress_message">Đang xóa…</string> <string name="track_delete_progress_message">Đang xóa…</string>
<!-- Track Detail --> <!-- Track Detail -->
@@ -466,4 +468,19 @@ limitations under the License.
<string name="about_version_code">Mã phiên bản: %1$d</string> <string name="about_version_code">Mã phiên bản: %1$d</string>
<string name="about_libraries">Thư viện</string> <string name="about_libraries">Thư viện</string>
<string name="help_feature_title">OpenTracks có những tính năng sau đây:</string> <string name="help_feature_title">OpenTracks có những tính năng sau đây:</string>
<string name="help_storage_pictures_content">Chúng được lưu trữ trong bộ nhớ trong của ứng dụng (vd: chỉ truy cập được từ trong OpenTracks).</string>
<string name="help_data_google_play_services_title">OpenTracks có yêu cầu/sử dụng Dịch vụ Google Play không\?</string>
<string name="help_recording_title">OpenTracks ghi lại dữ liệu như thế nào\?</string>
<string name="about_description"><i>OpenTracks</i> là một ứng dụng theo dõi thể thao hoàn toàn tôn trọng sự riêng tư của bạn.</string>
<string name="about_url">Trang web của dự án: <a href="%1$s">%1$s</a></string>
<string name="about_history">OpenTracks là một fork của <a href="https://code.google.com/archive/p/mytracks/"><i>MyTracks của Google</i></a> (bị ngừng tiếp tục vào năm 2016).</string>
<string name="help_feature_content">* tạo các tuyến đường GPS
\n* sử dụng cảm biến nhịp tim (chỉ Bluetooth LE)
\n* tuyên bố bằng giọng nói (thời gian hoặc khoảng cách)
\n* tạo đánh dấu (bao gồm hình ảnh)
\n* hoạt động hoàn toàn ngoại tuyến
\n* hiện đường đi trên bản đồ (xem bên dưới)</string>
<string name="help_track_map_title">Hiện tuyến đường trên bản đồ như thế nào\?</string>
<string name="about_third_party_data">Dữ liệu bên thứ ba</string>
<string name="help_storage_pictures_title">OpenTracks lưu trữ các ảnh được chụp làm đánh dấu như thế nào\?</string>
</resources> </resources>
+2
View File
@@ -352,6 +352,7 @@ limitations under the License.
<string name="menu_resume_track">Resume track</string> <string name="menu_resume_track">Resume track</string>
<!-- OpenTracks --> <!-- OpenTracks -->
<string name="recording_service">Recording service</string> <string name="recording_service">Recording service</string>
<string name="delete_service">Delete service</string>
<!-- Permission --> <!-- Permission -->
<string name="permission_gps_failed">OpenTracks requires permission to use GPS.</string> <string name="permission_gps_failed">OpenTracks requires permission to use GPS.</string>
<!-- Search --> <!-- Search -->
@@ -475,6 +476,7 @@ limitations under the License.
<string name="track_delete_one_confirm_title">Delete track?</string> <string name="track_delete_one_confirm_title">Delete track?</string>
<string name="track_delete_multiple_confirm_message">The selected tracks and their markers will be permanently deleted from the device.</string> <string name="track_delete_multiple_confirm_message">The selected tracks and their markers will be permanently deleted from the device.</string>
<string name="track_delete_progress_message">Deleting&#8230;</string> <string name="track_delete_progress_message">Deleting&#8230;</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<!-- Track Detail --> <!-- Track Detail -->
<string name="track_detail_chart_tab">Chart</string> <string name="track_detail_chart_tab">Chart</string>
<string name="track_detail_stats_tab">Stats</string> <string name="track_detail_stats_tab">Stats</string>