Compare commits

..

53 Commits

Author SHA1 Message Date
Dennis Guse 45874d3749 Release: v4.0.7 2022-08-21 21:55:19 +02:00
Peter Storch 594f69320a Fix dependencies to lifecycle-viewmodel 2022-08-21 19:28:07 +02:00
dependabot[bot] d4927c9444 Bump appcompat from 1.4.2 to 1.5.0
Bumps appcompat from 1.4.2 to 1.5.0.

---
updated-dependencies:
- dependency-name: androidx.appcompat:appcompat
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-21 19:28:07 +02:00
Alexander Ivanov 00dbd056d6 Translated using Weblate (Russian)
Currently translated at 99.0% (496 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2022-08-21 18:53:26 +02:00
M K 8f19ad8f66 Translated using Weblate (Arabic)
Currently translated at 53.6% (269 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ar/
2022-08-21 18:53:26 +02:00
mondstern 2fdad54a7c Translated using Weblate (German)
Currently translated at 94.8% (475 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2022-08-21 18:53:26 +02:00
Peter Storch 5a5f040ed5 Fix #1340 mass track deletion time 2022-08-21 18:53:20 +02:00
Dennis Guse 0ada9a2463 Android13: request NOTIFICATION_POST permission.
Fixes #1353.
Part of #1210.
2022-08-18 08:07:37 +02:00
Dennis Guse 719bb981ec Bugfix: Public API stop does not start service in foreground.
Fixes #1354.
2022-08-17 18:43:27 +02:00
dependabot[bot] 3c95d8744d Bump mockito-android from 4.6.1 to 4.7.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 4.6.1 to 4.7.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v4.6.1...v4.7.0)

---
updated-dependencies:
- dependency-name: org.mockito:mockito-android
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-15 07:59:40 +02:00
Dennis Guse 63d1c9ee5e GPS: set signal lost threshold to 30s. 2022-08-14 20:18:37 +02:00
Dennis Guse 1ccd2e3c0a Release: v4.0.6 2022-08-07 19:47:54 +02:00
Dennis Guse cdbddabc1b Cleanup: use ContextCompat.getColor() 2022-08-04 22:11:44 +02:00
Dennis Guse 9f6ca83f27 Bug: PublicAPI needs to call startForegroundService (SDK API26+).
Fixes #1301.
2022-08-04 22:11:44 +02:00
dependabot[bot] f2ee99a477 Bump gradle from 7.2.1 to 7.2.2
Bumps gradle from 7.2.1 to 7.2.2.

---
updated-dependencies:
- dependency-name: com.android.tools.build:gradle
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-04 06:44:02 +02:00
Dennis Guse 7009ede095 EGM2008 offset lookup for negative longitudes was wrong. 2022-08-03 17:30:31 +02:00
Dennis Guse 2b530a1c53 Cleanup: unused imports. 2022-08-02 18:44:15 +02:00
Poussinou 33673f5921 Update FUNDING.yml 2022-07-31 14:59:56 +02:00
Dennis Guse a7398d43e3 Release: v4.0.5 2022-07-29 17:58:32 +02:00
Dennis Guse 59b016a4a3 PlayStore: use an inverted icon (app launcher only).
Part of #1329.
2022-07-29 17:56:28 +02:00
Dennis Guse 78aa5d51c6 Intervals: if gain/loss is not available show "-" instead of null.
Fixes #1338.
2022-07-26 22:30:08 +02:00
Quang Trung 95f584b554 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (501 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/vi/
2022-07-21 15:04:36 +02:00
Dennis Guse cf759d526d README_TESTED_SENSORS.md: we now store sensor data without movement.
Part of #1088.
2022-07-18 07:35:46 +02:00
Dennis Guse 4c44ea12a4 Release: v4.0.4 2022-07-15 21:41:46 +02:00
dependabot[bot] 9d09865907 Bump reactivecircus/android-emulator-runner from 2.24.0 to 2.25.0
Bumps [reactivecircus/android-emulator-runner](https://github.com/reactivecircus/android-emulator-runner) from 2.24.0 to 2.25.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.24.0...v2.25.0)

---
updated-dependencies:
- dependency-name: reactivecircus/android-emulator-runner
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-13 07:39:30 +02:00
Dennis Guse 5667d2d83e Store sensor data without distance.
Fixes #1088.
2022-07-11 22:56:11 +02:00
Dennis Guse 386f017fb7 VoiceAnnouncement: avoid using deprecated API. 2022-07-10 21:46:15 +02:00
Román Ginés 87594e822d Bugfixed: adding a mark with a photo after rotating in camera resulted in a crash. Fixes #1321. 2022-07-10 19:47:45 +02:00
Dennis Guse ee832c7bd2 Revert upgrade ot AGP 7.3.0-beta. 2022-07-10 17:59:32 +02:00
Dennis Guse 30042d5bc3 Merge branch 'translations' 2022-07-09 19:49:28 +02:00
Hiajen 73cbb7a29c Translated using Weblate (German)
Currently translated at 75.5% (68 of 90 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/de/
2022-07-09 19:48:52 +02:00
Hiajen c100fdde4f Translated using Weblate (German)
Currently translated at 94.8% (475 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/de/
2022-07-09 19:48:52 +02:00
Dennis Guse 04e1496084 Cleanup: adjusted Interval uses constructor.
Fixes #1320.
2022-07-09 15:52:57 +02:00
Dennis Guse 21d1852505 Test: import+export results in same timezone.
Part of #1296.
2022-07-08 00:16:48 +02:00
Dennis Guse 1502b0cfa7 Gain/loss: handle events in our TrackRecordingService.handler (avoids potential race conditions). 2022-07-07 18:27:56 +02:00
Dennis Guse e7d6f65508 Gain/loss: use 5s as sensor interval to save some energy. 2022-07-07 18:27:56 +02:00
Dennis Guse 0e69a444a5 Add a comma. 2022-07-07 18:26:27 +02:00
Román Ginés 3b62999fde Announcement computes intervals from last track point. Fixes #645.
Bugfixed: intervals distance wasn't computed correctly.
2022-07-06 22:54:45 +02:00
Dennis Guse 97431233ac Fix translations. 2022-07-05 21:01:09 +02:00
Uglješa d49675afc8 Translated using Weblate (Serbian)
Currently translated at 56.6% (284 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/sr/
2022-07-03 18:29:42 +02:00
vsevjednom-cz c1f7ab36a8 Translated using Weblate (Czech)
Currently translated at 94.2% (472 of 501 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2022-07-03 18:29:42 +02:00
FTno eb14cc7bc6 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (500 of 500 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2022-07-03 18:29:42 +02:00
Xosé M a47b9164af Translated using Weblate (Galician)
Currently translated at 100.0% (500 of 500 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2022-07-03 18:29:42 +02:00
Surfoo 502e3ecb21 Translated using Weblate (French)
Currently translated at 100.0% (500 of 500 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/fr/
2022-07-03 18:29:42 +02:00
metezd 71903f174a Translated using Weblate (Turkish)
Currently translated at 98.0% (490 of 500 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/tr/
2022-07-03 18:29:41 +02:00
Dennis Guse ab6606a295 Github actions: rename APK build job. 2022-07-03 18:29:38 +02:00
Dennis Guse c4e2023992 Public API: allow setting track properties (name, category, description) on StartRecording.
Fixes #1282.
2022-07-03 17:32:21 +02:00
Dennis Guse cb10bdda3f Settings: show package id in Public API settings. 2022-07-03 10:28:43 +02:00
Dennis Guse f913472aae Settings: idle speed threshold has now a summary. 2022-07-03 10:28:31 +02:00
Dennis Guse af4826c578 TrackStoppedActivity: update track data on resume track.
Fixes #1249.
2022-07-02 08:51:31 +02:00
Dennis Guse 63172ec66f Github Actions: downgrade triggers. 2022-07-01 20:40:38 +02:00
Dennis Guse 3d6c339509 Android13: use new Image picker.
Part of #1210.
2022-07-01 20:24:20 +02:00
Dennis Guse 49f86c4eb9 Android13: upgrade target sdk to 33.
Part of #1210.
2022-07-01 20:24:20 +02:00
105 changed files with 857 additions and 418 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
liberapay: OpenTracks
github: [rgmf, pstorch, dennisguse]
custom: [ https://liberapay.com/OpenTracks, https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore ]
custom: https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore
@@ -1,9 +1,10 @@
#https://github.com/marketplace/actions/android-emulator-runner
name: Build nightly
on: [ push ]
name: Build APK
on:
push
jobs:
nightly-build:
apk-build:
runs-on: ubuntu-latest
steps:
- name: checkout
@@ -14,7 +15,7 @@ jobs:
distribution: 'adopt'
java-version: '11'
- name: Assemble nighlty APK
- name: Assemble APK
shell: bash
run: |
echo ${NIGHTLY_STORE_FILE} | base64 -d > KEY_NIGHTLY.jks
@@ -28,7 +29,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v3
with:
name: OpenTracks-nightly.apk
name: OpenTracks-APK.apk
path: ./build/outputs/apk/nightly/*.apk
retention-days: 7
+1 -1
View File
@@ -2,7 +2,7 @@ name: Linting
on:
push:
pull_request_target:
pull_request:
jobs:
analyse:
+2 -2
View File
@@ -2,7 +2,7 @@
name: Test
on:
push:
pull_request_target:
pull_request:
jobs:
test:
@@ -23,7 +23,7 @@ jobs:
java-version: '11'
- name: run tests
uses: reactivecircus/android-emulator-runner@v2.24.0
uses: reactivecircus/android-emulator-runner@v2.25.0
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
+24 -7
View File
@@ -95,7 +95,7 @@ OpenTracks can be used with [Gadgetbridge](https://www.gadgetbridge.org/):
__Only required permission:__
* _ACCESS_FINE_LOCATION_: required to use the GPS.
### API
### Public API
OpenTracks includes an API for starting/stopping recording by another installed application (e.g., [Automate](https://llamalab.com/automate/), [Tasker](https://tasker.joaoapps.com), or [Easer](https://github.com/renyuneyun/Easer)).
The API can be invoked by sending an explicit Intent to start an activity.
@@ -107,20 +107,37 @@ The API can be invoked by sending an explicit Intent to start an activity.
* Nightly: `de.dennisguse.opentracks.nightly`
`Class`:
* **Start a recording:** `de.dennisguse.opentracks.publicapi.StartRecording`
* **Stop a recording:** `de.dennisguse.opentracks.publicapi.StopRecording`
For testing via adb: `adb shell am start -n "package/class"`
The Start/Stop API is disabled by default to protect the user's privacy, but it can easily be enabled in the settings.
`StartRecording` supports the following parameters:
## Custom Dashboards (incl. map)
As of v3.3.1, OpenTracks supports custom dashboards for displaying previously recorded and live tracks.
* Set track data: `TRACK_NAME`, `TRACK_CATEGORY`, and `TRACK_DESCRIPTION`
* Send recorded data to another application via _Dashboard API_: `STATS_TARGET_PACKAGE`
and `STATS_TARGET_CLASS`
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates). The Dashboard API is also used by [Gadgetbridge](https://codeberg.org/Freeyourgadget/Gadgetbridge/) for displaying live track statistics on supported wearables.
The Public API is disabled by default to protect the user's privacy, but it can easily be enabled in
the settings.
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.
__IMPORTANT__: triggering `StartRecording` does not check if Android permissions (location or
Bluetooth) were granted. If they are not granted`
## Dashboard API (incl. map)
As of v3.3.1, OpenTracks supports custom dashboards for displaying previously recorded and live
tracks.
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which
presents an OpenStreetMap map (showing the current track, incl. updates). The Dashboard API is also
used by [Gadgetbridge](https://codeberg.org/Freeyourgadget/Gadgetbridge/) for displaying live track
statistics on supported wearables.
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.
The developer documentation is in [README_API.md](README_API.md).
-4
View File
@@ -38,10 +38,6 @@ However, often only one value is provided.
We only support _Instantaneous Power_.
_WARNING_: we only support physically movements with valid GPS coordinates.
Using a device indoors (no GPS available) will only show the current power values, but won't store anything.
Also the distance is not computed.
For cycling:
* Assioma Duo Powermeter
* Elite drivo II
+11 -8
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.1'
classpath 'com.android.tools.build:gradle:7.2.2'
}
}
@@ -51,7 +51,7 @@ android {
targetCompatibility JavaVersion.VERSION_11
}
compileSdkVersion 32
compileSdk 33
buildFeatures {
viewBinding true
@@ -59,13 +59,13 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 4951
versionName "v4.0.3"
versionCode 5004
versionName "v4.0.7"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdkVersion 21
targetSdkVersion 32
minSdk 21
targetSdk 33
testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
@@ -100,6 +100,7 @@ android {
releasePlayStore {
applicationIdSuffix ".playstore"
versionNameSuffix "-PlayStore"
signingConfig signingConfigs.debug
}
}
@@ -119,7 +120,9 @@ android {
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'androidx.appcompat:appcompat:1.5.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'com.google.android.material:material:1.6.1'
@@ -130,7 +133,7 @@ dependencies {
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'org.mockito:mockito-android:4.6.1'
androidTestImplementation 'org.mockito:mockito-android:4.7.0'
androidTestUtil 'androidx.test:orchestrator:1.4.1'
}
@@ -7,4 +7,4 @@ Funktionen & Features:
* zeichnet deine Herzfrequenz auf (Bluetooth LE)
* nutze beim Radfahren deine Trittfrequenz- und Geschwindigkeitssensoren sowie Leistungsmesser (Bluetooth LE)
* exportiert deine Daten als KML oder GPX
* funktioniert ganz ohne Internet
* funktioniert ganz ohne Internet
@@ -1 +1 @@
OpenTracks – ein GPS-Tracker, der deine Daten für sich behält.
OpenTracks – ein GPS-Tracker, der deine Daten für sich behält.
@@ -0,0 +1,13 @@
v4.0.4: OpenTracks
Changes:
- Store BLE sensor data even without distance (every 10s)
- Public API: allows setting name, category, and description
Bugfix:
- Crash when taking a picture and rotating the phone
-
Developer:
- Target SDK 33
- Barometer uses interval of 5s
@@ -0,0 +1,8 @@
v4.0.5: OpenTracks
Changes:
- PlayStore relase get's another logo
Bugfix:
- Interval list shows "-" if gain/loss is not present
@@ -0,0 +1,6 @@
v4.0.6: OpenTracks
Bugfix:
- EGM2008 offset computed wrong for negative longitudes
- PublicAPI crashed if OpenTracks was in background (API26+)
@@ -0,0 +1,11 @@
v4.0.7: OpenTracks
Changes:
- GPS signal lost timeout: 30s
- Android13: request permission to show notifications
- Faster deletion of tracks (vacuum database only if needed)
Bugfix:
- PublicAPI stop command resulted in crash (introduced in v4.0.6)
- Permissions: request all permissions during on startup
@@ -3,48 +3,25 @@ package de.dennisguse.opentracks;
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.longClick;
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;
import static de.dennisguse.opentracks.util.EspressoUtils.childAtPosition;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import static de.dennisguse.opentracks.util.EspressoUtils.withListSize;
import android.app.ActivityManager;
import android.app.Instrumentation;
import android.content.Context;
import android.view.View;
import android.widget.ListView;
import androidx.test.espresso.IdlingRegistry;
import androidx.test.espresso.IdlingResource;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.assertion.ViewAssertions;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.GrantPermissionRule;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.services.TrackDeleteService;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoDeleteTrackTest {
@@ -4,7 +4,6 @@ import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isClickable;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
@@ -15,6 +15,10 @@
*/
package de.dennisguse.opentracks.chart;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -25,10 +29,6 @@ import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Tests {@link ChartValueSeries}.
*
@@ -155,26 +155,31 @@ public class ExportImportTest {
mockAltitudeChange(trackPointCreator, 1);
mockBLESensorData(trackPointCreator, 15f, sensorDistance, 66f, 3f, 50f);
trackPointCreator.setClock("2020-02-02T02:02:05Z");
trackPointCreator.setClock("2020-02-02T02:02:14Z");
mockBLESensorData(trackPointCreator, 15f, null, 66f, 3f, 50f);
trackPointCreator.setClock("2020-02-02T02:02:15Z");
mockBLESensorData(trackPointCreator, null, null, 66f, 3f, 50f);
trackPointCreator.setClock("2020-02-02T02:02:16Z");
mockBLESensorData(trackPointCreator, 5f, Distance.of(2), 66f, 3f, 50f); // Distance will be added to next TrackPoint
sendLocation(trackPointCreator, "2020-02-02T02:02:05Z", 3, 14.001, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:02:17Z", 3, 14.001, 10, 13, 15, 10, 0);
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
trackPointCreator.setClock("2020-02-02T02:02:06Z");
trackPointCreator.setClock("2020-02-02T02:02:18Z");
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
service.endCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:02:20Z");
trackPointCreator.setClock("2020-02-02T02:03:20Z");
service.resumeTrack(trackId);
sendLocation(trackPointCreator, "2020-02-02T02:02:21Z", 3, 14.002, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:03:21Z", 3, 14.002, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:02:22Z", 3, 16, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:03:22Z", 3, 16, 10, 13, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:02:23Z", 3, 16.001, 10, 27, 15, 10, 0);
sendLocation(trackPointCreator, "2020-02-02T02:03:23Z", 3, 16.001, 10, 27, 15, 10, 0);
trackPointCreator.setClock("2020-02-02T02:02:24Z");
trackPointCreator.setClock("2020-02-02T02:03:24Z");
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
service.endCurrentTrack();
@@ -187,7 +192,7 @@ public class ExportImportTest {
track = contentProviderUtils.getTrack(trackId);
trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
markers = contentProviderUtils.getMarkers(trackId);
assertEquals(10, trackPoints.size());
assertEquals(12, trackPoints.size());
assertEquals(2, markers.size());
}
@@ -231,29 +236,29 @@ public class ExportImportTest {
// Time
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
assertEquals(Instant.parse("2020-02-02T02:02:02Z"), importedTrackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:02:24Z"), importedTrackStatistics.getStopTime());
assertEquals(Instant.parse("2020-02-02T02:03:24Z"), importedTrackStatistics.getStopTime());
TrackStatistics originalTrackStatistics = track.getTrackStatistics();
assertEquals(originalTrackStatistics.getTotalTime(), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(7), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(19), importedTrackStatistics.getTotalTime());
assertEquals(originalTrackStatistics.getMovingTime(), importedTrackStatistics.getMovingTime());
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(originalTrackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance());
assertEquals(123.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
assertEquals(125.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
// Speed
assertEquals(originalTrackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed());
assertEquals(41.05, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(41.72, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(originalTrackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed());
assertEquals(17.59, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(6.58, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(originalTrackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed());
assertEquals(41.05, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(41.72, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
// Altitude
assertEquals(originalTrackStatistics.getMinAltitude(), importedTrackStatistics.getMinAltitude(), 0.01);
@@ -345,19 +350,19 @@ public class ExportImportTest {
.setAltitudeLoss(1f)
.setAltitudeGain(1f)
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:05Z"))
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:17Z"))
.setLatitude(3)
.setLongitude(14.001)
.setAltitude(10)
.setSpeed(Speed.of(5))
.setAltitudeLoss(1f)
.setAltitudeGain(1f)
.setSensorDistance(Distance.of(12))
.setSensorDistance(Distance.of(14))
.setHeartRate(66f)
.setPower(50f)
.setCadence(3f)
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:21Z"))
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:03:21Z"))
.setLatitude(3)
.setLongitude(14.002)
.setAltitude(10)
@@ -365,7 +370,7 @@ public class ExportImportTest {
.setAltitudeGain(0f)
.setSpeed(Speed.of(15))
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:22Z"))
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:03:22Z"))
.setLatitude(3)
.setLongitude(16)
.setAltitude(10)
@@ -373,7 +378,7 @@ public class ExportImportTest {
.setAltitudeGain(0f)
.setSpeed(Speed.of(15))
.setHorizontalAccuracy(Distance.of(10)),
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:23Z"))
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:03:23Z"))
.setLatitude(3)
.setLongitude(16.001)
.setAltitude(10)
@@ -390,18 +395,18 @@ public class ExportImportTest {
// Time
assertEquals(track.getZoneOffset(), importedTrack.getZoneOffset());
assertEquals(Instant.parse("2020-02-02T02:02:03Z"), importedTrackStatistics.getStartTime());
assertEquals(Instant.parse("2020-02-02T02:02:23Z"), importedTrackStatistics.getStopTime());
assertEquals(Instant.parse("2020-02-02T02:03:23Z"), importedTrackStatistics.getStopTime());
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(3), importedTrackStatistics.getMovingTime());
assertEquals(Duration.ofSeconds(15), importedTrackStatistics.getTotalTime());
assertEquals(Duration.ofSeconds(15), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(123.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
assertEquals(125.16, importedTrackStatistics.getTotalDistance().toM(), 0.01);
// Speed
assertEquals(41.05, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(41.05, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(41.05, importedTrackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
assertEquals(8.34, importedTrackStatistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(8.34, importedTrackStatistics.getAverageSpeed().toMPS(), 0.01);
assertEquals(8.34, importedTrackStatistics.getAverageMovingSpeed().toMPS(), 0.01);
// Altitude
assertEquals(10, importedTrackStatistics.getMinAltitude(), 0.01);
@@ -15,11 +15,16 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.stream.Collectors;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
@@ -27,6 +32,8 @@ import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
@@ -208,4 +215,31 @@ public class GPXTrackImporterTest {
.setSpeed(Speed.of(3))
), importedTrackPoints);
}
@LargeTest
@Test
public void importExportTest_timezone() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.gpx_timezone);
InputStream inputStreamExpected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.gpx_timezone);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
TrackExporter trackExporter = TrackFileFormat.GPX.createTrackExporter(context);
trackExporter.writeTrack(importedTrack, outputStream);
// then
String expected = new BufferedReader(
new InputStreamReader(inputStreamExpected, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n")) + "\n";
assertEquals(expected, outputStream.toString()); //TODO inputStream.readAllBytes() ?
}
}
@@ -31,12 +31,14 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.HeartRate;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.io.file.importer.TrackPointAssert;
import de.dennisguse.opentracks.sensors.AltitudeSumManager;
import de.dennisguse.opentracks.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.settings.PreferencesUtils;
@@ -257,6 +259,47 @@ public class TrackRecordingServiceTestRecording {
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
public void testRecording_blesensor_only_no_distance() {
// given
String startTime = "2020-02-02T02:02:02Z";
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
trackPointCreator.setClock(startTime);
Track.Id trackId = service.startNewTrack();
trackPointCreator.stopGPS();
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
BluetoothRemoteSensorManager remoteSensorManager = trackPointCreator.getRemoteSensorManager();
// when
String sensor1 = "2020-02-02T02:02:03Z";
trackPointCreator.setClock(sensor1);
remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(5))); //Should be ignored
String sensor3 = "2020-02-02T02:02:13Z";
trackPointCreator.setClock(sensor3);
remoteSensorManager.onChanged(new SensorDataHeartRate("", "", HeartRate.of(7)));
String stopTime = "2020-02-02T02:02:15Z";
trackPointCreator.setClock(stopTime);
service.endCurrentTrack();
// then
new TrackPointAssert().assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
new TrackPoint(TrackPoint.Type.SENSORPOINT, Instant.parse(sensor3))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate(HeartRate.of(7)),
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
.setAltitudeGain(0f)
.setAltitudeLoss(0f)
.setHeartRate(HeartRate.of(7))
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
}
@MediumTest
@Test
public void testRecording_gpsOnly_recordingDistance_above() {
@@ -142,7 +142,7 @@ public class EGM2008UtilsTest {
}
@Test
public void data_Berlin() throws IOException {
public void data_Berlin_Germany_() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(52.530644);
@@ -157,7 +157,7 @@ public class EGM2008UtilsTest {
}
@Test
public void data_Berlin_Caching() throws IOException {
public void data_Berlin_Germany_Caching() throws IOException {
// given
TrackPoint trackPoint1 = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint1.setLatitude(52.530644);
@@ -176,6 +176,22 @@ public class EGM2008UtilsTest {
assertNotEquals(altitude_egm2008.correctAltitude(trackPoint1.getLocation()), altitude_egm2008.correctAltitude(trackPoint2.getLocation()), 0.0001);
}
@Test
public void data_Seattle_USA() throws IOException {
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
trackPoint.setLatitude(47.63153);
trackPoint.setLongitude(-122.30938);
trackPoint.setAltitude(0);
// when
EGM2008Utils.EGM2008Correction altitude_egm2008 = EGM2008Utils.createCorrection(context, trackPoint.getLocation());
// then
assertEquals(22.99, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void data_MaxUndulation() throws IOException {
// given
@@ -191,6 +207,7 @@ public class EGM2008UtilsTest {
assertEquals(-85.824, altitude_egm2008.correctAltitude(trackPoint.getLocation()), MAX_BILINEAR_ERROR);
}
@Test
public void getIndices() {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.ofEpochMilli(0));
@@ -208,7 +225,7 @@ public class EGM2008UtilsTest {
trackPoint.setLatitude(-90);
trackPoint.setLongitude(-180);
assertEquals(new EGM2008Utils.Indices(2160, 0), EGM2008Utils.getIndices(trackPoint.getLocation()));
assertEquals(new EGM2008Utils.Indices(2160, 2160), EGM2008Utils.getIndices(trackPoint.getLocation()));
}
@Test
+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx
version="1.1"
creator="OpenTracks (Debug)"
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:topografix="http://www.topografix.com/GPX/Private/TopoGrafix/0/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1"
xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v2"
xmlns:gpxtrkx="http://www.garmin.com/xmlschemas/TrackStatsExtension/v1"
xmlns:cluetrust="http://www.cluetrust.com/Schemas/"
xmlns:pwr="http://www.garmin.com/xmlschemas/PowerExtension/v1"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.topografix.com/GPX/Private/TopoGrafix/0/1 http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v2 https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 https://www8.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.garmin.com/xmlschemas/TrackStatsExtension/v1 http://www.cluetrust.com/Schemas http://www.cluetrust.com/Schemas/gpxdata10.xsd http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd">
<wpt lat="3" lon="14">
<ele>10</ele>
<time>2021-01-07T21:51:59.179+01:15</time>
<name><![CDATA[Marker 1]]></name>
<desc><![CDATA[Marker 1 desc]]></desc>
<type><![CDATA[Marker 1 category]]></type>
</wpt>
<trk>
<name><![CDATA[2021-01-07 22:51]]></name>
<desc><![CDATA[the description]]></desc>
<type><![CDATA[the category]]></type>
<extensions>
<topografix:color>c0c0c0</topografix:color>
<opentracks:trackid>7002101e-4198-4613-8c24-544e01ca3981</opentracks:trackid>
<gpxtrkx:TrackStatsExtension>
<gpxtrkx:Distance>0.0</gpxtrkx:Distance>
<gpxtrkx:TimerTime>0</gpxtrkx:TimerTime>
<gpxtrkx:MovingTime>0</gpxtrkx:MovingTime>
<gpxtrkx:StoppedTime>0</gpxtrkx:StoppedTime>
<gpxtrkx:MaxSpeed>0.0</gpxtrkx:MaxSpeed>
<gpxtrkx:Ascent>null</gpxtrkx:Ascent>
<gpxtrkx:Descent>null</gpxtrkx:Descent>
</gpxtrkx:TrackStatsExtension>
</extensions>
<trkseg>
<trkpt lat="3" lon="14">
<ele>10</ele>
<time>2021-01-07T21:51:59.179+01:15</time>
</trkpt>
</trkseg>
</trk>
</gpx>
+2
View File
@@ -59,6 +59,8 @@ limitations under the License.
tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name=".Startup"
android:allowBackup="false"
@@ -84,8 +84,8 @@ public abstract class AbstractTrackDeleteActivity extends AbstractActivity imple
/**
* Called every time a track is deleted.
*/
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
if (deleteStatus.isFinished() && trackDeleteServiceConnection != null) {
protected void onTrackDeleteStatus(TrackDeleteService.DeletionFinishedStatus deletionFinishedStatus) {
if (trackDeleteServiceConnection != null) {
trackDeleteServiceConnection.unbind(this);
trackDeleteServiceConnection = null;
onDeleteFinished();
@@ -3,19 +3,15 @@ package de.dennisguse.opentracks;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import de.dennisguse.opentracks.databinding.ActivityShowErrorBinding;
import de.dennisguse.opentracks.databinding.TrackListBinding;
public class ShowErrorActivity extends AbstractActivity {
@@ -70,7 +70,7 @@ import de.dennisguse.opentracks.ui.util.ActivityUtils;
import de.dennisguse.opentracks.ui.util.ListItemUtils;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PermissionUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -241,7 +241,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
startActivity(newIntent);
connection.unbind(this);
}).startAndBind(this);
}).startAndBind(this, true);
});
viewBinding.trackListFabAction.setOnLongClickListener((view) -> {
if (!recordingStatus.isRecording()) {
@@ -263,13 +263,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
}
private void requestRequiredPermissions() {
if (!PermissionUtils.hasGPSPermission(this)) {
PermissionUtils.requestGPSPermission(this, null, () -> Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show());
}
if (!PermissionUtils.hasBluetoothPermissions(this)) {
PermissionUtils.requestBluetoothPermission(this, null, null);
}
PermissionRequester.ALL.requestPermissionsIfNeeded(this, this, null, (requester) -> Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show());
}
@Override
@@ -323,8 +317,8 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
searchView.findViewById(R.id.search_edit_frame).setPadding(0, 0, 48, 0);
SearchView.SearchAutoComplete searchAutoComplete = searchView.findViewById(R.id.search_src_text);
searchAutoComplete.setHintTextColor(getResources().getColor(android.R.color.white));
searchAutoComplete.setTextColor(getResources().getColor(android.R.color.white));
searchAutoComplete.setHintTextColor(ContextCompat.getColor(this, android.R.color.white));
searchAutoComplete.setTextColor(ContextCompat.getColor(this, android.R.color.white));
searchView.setOnCloseListener(() -> {
searchView.clearFocus();
@@ -213,7 +213,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
connection.unbind(this);
finish();
}).startAndBind(this);
}).startAndBind(this, true);
return true;
}
@@ -232,9 +232,9 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
}
@Override
protected void onTrackDeleteStatus(TrackDeleteService.DeleteStatus deleteStatus) {
super.onTrackDeleteStatus(deleteStatus);
if (deleteStatus.isDeleted(trackId)) {
protected void onTrackDeleteStatus(TrackDeleteService.DeletionFinishedStatus deletionFinishedStatus) {
super.onTrackDeleteStatus(deletionFinishedStatus);
if (deletionFinishedStatus.isDeleted(trackId)) {
runOnUiThread(this::finish);
}
}
@@ -11,8 +11,6 @@ import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.widget.Toast;
@@ -49,7 +47,6 @@ import de.dennisguse.opentracks.ui.markers.MarkerEditActivity;
import de.dennisguse.opentracks.ui.markers.MarkerListActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PermissionUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils;
@@ -138,7 +135,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
viewBinding.trackRecordingFabAction.setImageResource(R.drawable.ic_baseline_stop_24);
viewBinding.trackRecordingFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.opentracks_secondary_color));
viewBinding.trackRecordingFabAction.setBackgroundColor(getResources().getColor(R.color.opentracks_secondary_color));
viewBinding.trackRecordingFabAction.setBackgroundColor(ContextCompat.getColor(this, R.color.opentracks_secondary_color));
viewBinding.trackRecordingFabAction.setOnLongClickListener((view) -> {
((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).vibrate(1000);
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this);
@@ -90,18 +90,25 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
}
viewBinding.finishButton.setOnClickListener(v -> {
TrackUtils.updateTrack(TrackStoppedActivity.this, track, viewBinding.trackEditName.getText().toString(),
viewBinding.trackEditActivityType.getText().toString(), viewBinding.trackEditDescription.getText().toString(),
contentProviderUtils);
storeTrackMetaData(contentProviderUtils, track);
ExportUtils.postWorkoutExport(this, trackId);
finish();
});
viewBinding.resumeButton.setOnClickListener(v -> resumeTrackAndFinish());
viewBinding.resumeButton.setOnClickListener(v -> {
storeTrackMetaData(contentProviderUtils, track);
resumeTrackAndFinish();
});
viewBinding.discardButton.setOnClickListener(v -> ConfirmDeleteDialogFragment.showDialog(getSupportFragmentManager(), trackId));
}
private void storeTrackMetaData(ContentProviderUtils contentProviderUtils, Track track) {
TrackUtils.updateTrack(TrackStoppedActivity.this, track, viewBinding.trackEditName.getText().toString(),
viewBinding.trackEditActivityType.getText().toString(), viewBinding.trackEditDescription.getText().toString(),
contentProviderUtils);
}
@Override
public void onBackPressed() {
if (isDiscarding) {
@@ -138,7 +145,7 @@ public class TrackStoppedActivity extends AbstractTrackDeleteActivity implements
connection.unbind(this);
finish();
}).startAndBind(this);
}).startAndBind(this, true);
}
@Override
@@ -32,9 +32,7 @@ import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.TrackDataHub;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.databinding.ChartBinding;
@@ -3,7 +3,6 @@ package de.dennisguse.opentracks.chart;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.data.models.Altitude;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint;
@@ -24,6 +24,7 @@ import android.graphics.Paint.Style;
import android.graphics.Path;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import java.text.NumberFormat;
@@ -68,12 +69,12 @@ abstract class ChartValueSeries {
fillPaint = new Paint();
fillPaint.setStyle(Style.FILL);
fillPaint.setColor(context.getResources().getColor(fillColor));
fillPaint.setColor(ContextCompat.getColor(context, fillColor));
fillPaint.setAntiAlias(true);
strokePaint = new Paint();
strokePaint.setStyle(Style.STROKE);
strokePaint.setColor(context.getResources().getColor(strokeColor));
strokePaint.setColor(ContextCompat.getColor(context, strokeColor));
strokePaint.setAntiAlias(true);
// Make copies of the stroke paint with the default thickness
@@ -187,7 +187,6 @@ public class ContentProviderUtils {
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
}
// Delete track last since it triggers a database vacuum call
String whereClause = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
contentResolver.delete(TracksColumns.CONTENT_URI, whereClause, trackIds.stream().map(id -> Long.toString(id.getId())).toArray(String[]::new));
}
@@ -195,8 +194,6 @@ public class ContentProviderUtils {
public void deleteTrack(Context context, @NonNull Track.Id trackId) {
// Delete track folder resources.
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
// Delete track last since it triggers a database vacuum call
contentResolver.delete(TracksColumns.CONTENT_URI, TracksColumns._ID + "=?", new String[]{Long.toString(trackId.getId())});
}
@@ -27,6 +27,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
@@ -39,6 +40,7 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* A {@link ContentProvider} that handles access to track points, tracks, and markers tables.
@@ -53,6 +55,8 @@ public class CustomContentProvider extends ContentProvider {
private static final String SQL_LIST_DELIMITER = ",";
private static final int TOTAL_DELETED_ROWS_VACUUM_THRESHOLD = 10000;
private final UriMatcher uriMatcher;
private SQLiteDatabase db;
@@ -131,14 +135,12 @@ public class CustomContentProvider extends ContentProvider {
@Override
public int delete(@NonNull Uri url, String where, String[] selectionArgs) {
String table;
boolean shouldVacuum = false;
switch (getUrlType(url)) {
case TRACKPOINTS:
table = TrackPointsColumns.TABLE_NAME;
break;
case TRACKS:
table = TracksColumns.TABLE_NAME;
shouldVacuum = true;
break;
case MARKERS:
table = MarkerColumns.TABLE_NAME;
@@ -147,23 +149,40 @@ public class CustomContentProvider extends ContentProvider {
throw new IllegalArgumentException("Unknown URL " + url);
}
Log.w(TAG, "Deleting table " + table);
int count;
Log.w(TAG, "Deleting from table " + table);
int totalChangesBefore = getTotalChanges();
int deletedRowsFromTable;
try {
db.beginTransaction();
count = db.delete(table, where, selectionArgs);
deletedRowsFromTable = db.delete(table, where, selectionArgs);
Log.i(TAG, "Deleted " + deletedRowsFromTable + " rows of table " + table);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, false);
if (shouldVacuum) {
// If a potentially large amount of data was deleted, reclaim its space.
Log.i(TAG, "Vacuuming the database.");
int totalChanges = getTotalChanges() - totalChangesBefore;
Log.i(TAG, "Deleted " + totalChanges + " total rows from database");
PreferencesUtils.addTotalRowsDeleted(totalChanges);
int totalRowsDeleted = PreferencesUtils.getTotalRowsDeleted();
if (totalRowsDeleted > TOTAL_DELETED_ROWS_VACUUM_THRESHOLD) {
Log.i(TAG, "TotalRowsDeleted " + totalRowsDeleted + ", starting to vacuum the database.");
db.execSQL("VACUUM");
PreferencesUtils.resetTotalRowsDeleted();
}
return count;
return deletedRowsFromTable;
}
private int getTotalChanges() {
int totalCount;
try (Cursor cursor = db.rawQuery("SELECT total_changes()", null)) {
cursor.moveToNext();
totalCount = cursor.getInt(0);
}
return totalCount;
}
@Override
@@ -32,7 +32,6 @@ import java.util.HashSet;
import java.util.Set;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.data.tables.MarkerColumns;
@@ -13,6 +13,14 @@ public class Distance {
return new Distance(distance_m);
}
public static Distance of(Double distance_m) {
if (distance_m == null) {
return Distance.of(Double.NaN);
} else {
return Distance.of((double) distance_m);
}
}
public static Distance of(String distance_m) {
return of(Float.parseFloat(distance_m));
}
@@ -42,12 +42,13 @@ public class DistanceFormatter {
*/
public Pair<String, String> getDistanceParts(Distance distance) {
if (distance.isInvalid()) {
String valueUnknown = resources.getString(R.string.value_unknown);
switch (unitSystem) {
case METRIC:
return new Pair<>(null, resources.getString(R.string.unit_meter));
return new Pair<>(valueUnknown, resources.getString(R.string.unit_meter));
case IMPERIAL:
case NAUTICAL_IMPERIAL:
return new Pair<>(null, resources.getString(R.string.unit_feet));
return new Pair<>(valueUnknown, resources.getString(R.string.unit_feet));
default:
throw new RuntimeException("Not implemented");
}
@@ -56,7 +56,7 @@ public class TrackPoint {
SEGMENT_START_AUTOMATIC(-1), //Start of a segment due to too much distance from previous TrackPoint
TRACKPOINT(0), //Just GPS data and may contain BLE sensor data
SENSORPOINT(2), //Just BLE sensor data; required to have speed and sensorDistance
SENSORPOINT(2), //Just BLE sensor data; may have speed and sensorDistance
SEGMENT_END_MANUAL(1); //End of a segment
@@ -17,8 +17,8 @@ import androidx.documentfile.provider.DocumentFile;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.io.file.exporter.ExportActivity;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.IntentUtils;
public abstract class DirectoryChooserActivity extends AppCompatActivity {
@@ -25,6 +25,7 @@ import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.data.models.Speed;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
@@ -290,4 +291,5 @@ public class TrackImporter {
public void cleanImport() {
contentProviderUtils.deleteTracks(context, trackIds);
}
}
@@ -34,7 +34,7 @@ public abstract class AbstractAPIActivity extends AppCompatActivity {
if (PreferencesUtils.isPublicAPIenabled()) {
Log.i(TAG, "Received and trying to execute requested action.");
new TrackRecordingServiceConnection(serviceConnectedCallback)
.startAndBind(this);
.startAndBind(this, isStartServiceForeground());
} else {
Toast.makeText(this, getString(R.string.settings_public_api_disabled_toast), Toast.LENGTH_LONG).show();
Log.w(TAG, "Public API is disabled; ignoring request.");
@@ -42,6 +42,10 @@ public abstract class AbstractAPIActivity extends AppCompatActivity {
}
}
protected boolean isStartServiceForeground() {
return false;
}
protected abstract void execute(TrackRecordingService service);
protected abstract boolean isPostExecuteStopService();
@@ -2,28 +2,64 @@ package de.dennisguse.opentracks.publicapi;
import android.os.Bundle;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.TrackUtils;
public class StartRecording extends AbstractAPIActivity {
public static final String EXTRA_TRACK_NAME = "TRACK_NAME";
public static final String EXTRA_TRACK_CATEGORY = "TRACK_CATEGORY";
public static final String EXTRA_TRACK_DESCRIPTION = "TRACK_DESCRIPTION";
public static final String EXTRA_STATS_TARGET_PACKAGE = "STATS_TARGET_PACKAGE";
public static final String EXTRA_STATS_TARGET_CLASS = "STATS_TARGET_CLASS";
private static final String TAG = StartRecording.class.getSimpleName();
protected void execute(TrackRecordingService service) {
Track.Id trackId = service.startNewTrack();
if ((trackId != null) && (PreferencesUtils.isPublicAPIDashboardEnabled())) {
if (trackId != null) {
Bundle bundle = getIntent().getExtras();
if (bundle != null) {
String targetPackage = bundle.getString("STATS_TARGET_PACKAGE", null);
String targetClass = bundle.getString("STATS_TARGET_CLASS", null);
IntentDashboardUtils.startDashboard(this, true, targetPackage, targetClass, trackId);
updateTrackMetadata(trackId, bundle);
if (PreferencesUtils.isPublicAPIDashboardEnabled()) {
startDashboardAPI(trackId, bundle);
}
}
}
}
private void updateTrackMetadata(@NonNull Track.Id trackId, @NonNull Bundle bundle) {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
Track track = contentProviderUtils.getTrack(trackId);
TrackUtils.updateTrack(this, track, bundle.getString(EXTRA_TRACK_NAME, null),
bundle.getString(EXTRA_TRACK_CATEGORY, null), bundle.getString(EXTRA_TRACK_DESCRIPTION, null),
contentProviderUtils);
}
private void startDashboardAPI(@NonNull Track.Id trackId, @NonNull Bundle bundle) {
String targetPackage = bundle.getString(EXTRA_STATS_TARGET_PACKAGE, null);
String targetClass = bundle.getString(EXTRA_STATS_TARGET_CLASS, null);
if (targetClass != null && targetPackage != null) {
IntentDashboardUtils.startDashboard(this, true, targetPackage, targetClass, trackId);
}
}
@Override
protected boolean isPostExecuteStopService() {
return false;
}
@Override
protected boolean isStartServiceForeground() {
return true;
}
}
@@ -5,12 +5,15 @@ import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.util.concurrent.TimeUnit;
import de.dennisguse.opentracks.data.models.TrackPoint;
/**
@@ -29,7 +32,7 @@ public class AltitudeSumManager implements SensorEventListener {
private Float altitudeGain_m;
private Float altitudeLoss_m;
public void start(Context context) {
public void start(Context context, Handler handler) {
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
@@ -37,7 +40,7 @@ public class AltitudeSumManager implements SensorEventListener {
Log.w(TAG, "No pressure sensor available.");
isConnected = false;
} else {
isConnected = sensorManager.registerListener(this, pressureSensor, SensorManager.SENSOR_DELAY_FASTEST);
isConnected = sensorManager.registerListener(this, pressureSensor, (int) TimeUnit.SECONDS.toMicros(5), handler);
}
lastAcceptedPressureValue_hPa = Float.NaN;
@@ -35,7 +35,7 @@ import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
/**
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
@@ -162,7 +162,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
} else {
connectionManager.disconnect();
}
if (!PermissionUtils.hasBluetoothPermissions(context)) {
if (!PermissionRequester.BLUETOOTH.hasPermission(context)) {
Log.w(TAG, "BLUETOOTH_SCAN and/or BLUETOOTH_CONNECT not granted; not connecting.");
}
@@ -15,12 +15,14 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.settings.PreferencesUtils;
public class TrackDeleteService extends Service {
@@ -31,7 +33,7 @@ public class TrackDeleteService extends Service {
private final Binder binder = new Binder();
private ExecutorService serviceExecutor;
private MutableLiveData<DeleteStatus> deleteResultObservable;
private MutableLiveData<DeletionFinishedStatus> deleteResultObservable;
private NotificationManager notificationManager;
private NotificationCompat.Builder notificationBuilder;
@@ -59,22 +61,18 @@ public class TrackDeleteService extends Service {
return START_NOT_STICKY;
}
private void deleteTracks(@NonNull ArrayList<Track.Id> trackIds) {
sendResult(null,0, trackIds.size());
private void deleteTracks(@NonNull List<Track.Id> trackIds) {
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());
}
contentProviderUtils.deleteTracks(this, trackIds);
sendResult(trackIds);
stopSelf();
});
}
private void sendResult(Track.Id trackId, int deletes, int total) {
private void sendResult(List<Track.Id> trackIds) {
if (deleteResultObservable != null) {
deleteResultObservable.postValue(new DeleteStatus(trackId, deletes, total));
deleteResultObservable.postValue(new DeletionFinishedStatus(trackIds));
}
}
@@ -83,7 +81,7 @@ public class TrackDeleteService extends Service {
return binder;
}
public LiveData<DeleteStatus> getDeletingStatusObservable() {
public LiveData<DeletionFinishedStatus> getDeletingStatusObservable() {
return deleteResultObservable;
}
@@ -106,25 +104,13 @@ public class TrackDeleteService extends Service {
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))
.setContentText(this.getString(R.string.track_delete_number_of_tracks, tracksToDelete))
.setSmallIcon(R.drawable.ic_logo_color_24dp)
.setProgress(tracksToDelete, 0, false);
.setProgress(0, 0, true);
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() {
@@ -136,36 +122,25 @@ public class TrackDeleteService extends Service {
}
}
public static class DeleteStatus {
private final int progress;
private final int max;
private final Track.Id trackId;
public static class DeletionFinishedStatus {
private final List<Track.Id> trackIds;
/**
* @param trackId Track.Id just deleted if any.
* @param progress number of deletes.
* @param max total of deletes to be done.
* @param trackIds List of deleted Track.Ids.
*/
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;
private DeletionFinishedStatus(@Nullable List<Track.Id> trackIds) {
this.trackIds = trackIds;
}
public boolean isDeleted(Track.Id trackId) {
return this.trackId != null && this.trackId.equals(trackId);
return this.trackIds != null && this.trackIds.contains(trackId);
}
@NonNull
@Override
public String toString() {
return "DeleteStatus{" +
"number of deletes=" + progress +
", total=" + max +
"trackIds=" + trackIds +
'}';
}
}
@@ -10,6 +10,7 @@ import android.util.Pair;
import androidx.annotation.NonNull;
import java.time.Duration;
import java.time.ZoneOffset;
import de.dennisguse.opentracks.R;
@@ -180,8 +181,16 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
}
if (!trackPoint.hasLocation() && !trackPoint.hasSensorDistance()) {
Log.d(TAG, "Ignoring TrackPoint as it has no distance.");
return false;
Duration minStorageInterval = Duration.ofSeconds(10); // TODO Should be configurable.
boolean shouldStore = lastStoredTrackPoint.getTime().plus(minStorageInterval)
.isBefore(trackPoint.getTime());
if (!shouldStore) {
Log.d(TAG, "Ignoring TrackPoint as it has no distance (and sensor data is not new enough).");
return false;
} else {
insertTrackPoint(trackPoint, true);
return true;
}
}
Distance distanceToLastStoredTrackPoint;
@@ -129,7 +129,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
trackRecordingManager = null;
// Reverse order from onCreate
showNotification(false); //TODO Why?
stopForeground(true);
notificationManager.stop();
notificationManager = null;
@@ -205,7 +206,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
private void startSensors() {
wakeLock = SystemUtils.acquireWakeLock(this, wakeLock);
trackPointCreator.start(this, handler);
showNotification(true);
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this));
}
public void endCurrentTrack() {
@@ -247,7 +248,8 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
if (!isRecording()) return;
trackPointCreator.stop();
showNotification(false);
stopForeground(true);
notificationManager.cancelNotification();
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
}
@@ -273,19 +275,6 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
}
}
private void showNotification(boolean isGpsStarted) {
if (isRecording()) {
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setRecording(this, recordingStatus.getTrackId()));
}
if (!isRecording() && isGpsStarted) {
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this));
}
if (!isRecording() && !isGpsStarted) {
stopForeground(true);
notificationManager.cancelNotification();
}
}
public Marker.Id insertMarker(String name, String category, String description, String photoUrl) {
if (!isRecording()) {
return null;
@@ -28,6 +28,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.R;
@@ -66,15 +67,21 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
/**
* Starts and binds the service.
*
* @param foreground is the service expected to call `startForeground()`?
*/
public void startAndBind(Context context) {
public void startAndBind(Context context, boolean foreground) {
if (trackRecordingService != null) {
// Service is already started and bound.
return;
}
Log.i(TAG, "Starting the service.");
context.startService(new Intent(context, TrackRecordingService.class));
if (foreground) {
ContextCompat.startForegroundService(context, new Intent(context, TrackRecordingService.class));
} else {
context.startService(new Intent(context, TrackRecordingService.class));
}
startConnection(context);
}
@@ -88,7 +95,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
@Deprecated
public void startAndBindWithCallback(Context context) {
if (trackRecordingService == null) {
startAndBind(context);
startAndBind(context, false);
return;
}
if (callback != null) {
@@ -68,6 +68,7 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
}
void stop() {
cancelNotification();
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(this);
}
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks.services.announcement;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.speech.tts.TextToSpeech;
@@ -34,8 +35,8 @@ import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.TrackPointIterator;
import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.SensorStatistics;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
@@ -56,6 +57,10 @@ public class VoiceAnnouncement {
private final AudioManager audioManager;
private final ContentProviderUtils contentProviderUtils;
private TrackPoint.Id startTrackPointId = null;
private IntervalStatistics intervalStatistics;
private Distance intervalDistance;
private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
@@ -116,6 +121,8 @@ public class VoiceAnnouncement {
this.context = context;
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
contentProviderUtils = new ContentProviderUtils(context);
intervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
intervalStatistics = new IntervalStatistics(intervalDistance);
}
public void start() {
@@ -132,7 +139,10 @@ public class VoiceAnnouncement {
if (ttsFallback == null) {
Log.w(TAG, "MediaPlayer for ttsFallback could not be created.");
} else {
ttsFallback.setAudioStreamType(AUDIO_STREAM);
ttsFallback.setAudioAttributes(
new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build());
ttsFallback.setLooping(false);
}
}
@@ -165,21 +175,22 @@ public class VoiceAnnouncement {
return;
}
Distance currentIntervalDistance = PreferencesUtils.getVoiceAnnouncementDistance();
if (currentIntervalDistance != intervalDistance) {
intervalStatistics = new IntervalStatistics(currentIntervalDistance);
intervalDistance = currentIntervalDistance;
startTrackPointId = null;
}
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
boolean isReportSpeed = PreferencesUtils.isReportSpeed(track.getCategory());
//TODO Do not load all trackpoints for every announcement
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(unitSystem));
intervalStatistics.addTrackPoints(trackPointIterator);
TrackPointIterator trackPointIterator = new TrackPointIterator(contentProviderUtils, track.getId(), startTrackPointId);
startTrackPointId = intervalStatistics.addTrackPoints(trackPointIterator);
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
SensorStatistics sensorStatistics = null;
if (track.getId() != null) {
sensorStatistics = contentProviderUtils.getSensorStats(track.getId());
}
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), unitSystem, isReportSpeed, lastInterval, sensorStatistics);
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), PreferencesUtils.getUnitSystem(), PreferencesUtils.isReportSpeed(track.getCategory()), lastInterval, sensorStatistics);
if (announcement.length() > 0) {
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
@@ -5,8 +5,8 @@ import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnno
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageSpeedPace;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapHeartRate;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapSpeedPace;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTotalDistance;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceMovingTime;
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTotalDistance;
import android.content.Context;
import android.text.Spannable;
@@ -19,7 +19,7 @@ import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.PermissionUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener, SharedPreferences.OnSharedPreferenceChangeListener {
@@ -54,7 +54,7 @@ public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener
//TODO upgrade to AGP7.0.0/API31 started complaining about removeUpdates.
public void onStop() {
if (locationManager != null && context != null) {
if (PermissionUtils.hasGPSPermission(context)) {
if (PermissionRequester.GPS.hasPermission(context)) {
locationManager.removeUpdates(this);
}
locationManager = null;
@@ -152,7 +152,7 @@ public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener
Log.e(TAG, "Not started.");
return;
}
if (PermissionUtils.hasGPSPermission(context)) {
if (PermissionRequester.GPS.hasPermission(context)) {
try {
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, gpsInterval.toMillis(), 0, this);
} catch (SecurityException e) {
@@ -23,7 +23,7 @@ class GpsStatus {
private static final String TAG = GpsStatus.class.getSimpleName();
// The duration that GpsStatus waits from minimal interval to consider GPS lost.
private static final Duration SIGNAL_LOST_THRESHOLD = Duration.ofSeconds(10);
private static final Duration SIGNAL_LOST_THRESHOLD = Duration.ofSeconds(30);
private Distance thresholdHorizontalAccuracy;
// Threshold for time without points.
@@ -58,7 +58,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
altitudeSumManager = new AltitudeSumManager();
remoteSensorManager.start();
altitudeSumManager.start(context);
altitudeSumManager.start(context, handler);
}
@@ -8,9 +8,6 @@ import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
public class MainSettingsFragment extends PreferenceFragmentCompat {
@@ -826,4 +826,17 @@ public class PreferencesUtils {
return getString(R.string.show_on_map_format_key, IntentDashboardUtils.PREFERENCE_ID_ASK);
}
public static int getTotalRowsDeleted() {
return getInt(R.string.total_rows_deleted_key, 0);
}
public static void addTotalRowsDeleted(final int totalRowsDeletedToAdd) {
int newTotalRowsDeleted = getTotalRowsDeleted() + totalRowsDeletedToAdd;
setInt(R.string.total_rows_deleted_key, newTotalRowsDeleted);
}
public static void resetTotalRowsDeleted() {
setInt(R.string.total_rows_deleted_key, 0);
}
}
@@ -4,6 +4,7 @@ import android.os.Bundle;
import androidx.preference.PreferenceFragmentCompat;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.R;
public class PublicAPISettingsFragment extends PreferenceFragmentCompat {
@@ -17,5 +18,7 @@ public class PublicAPISettingsFragment extends PreferenceFragmentCompat {
public void onStart() {
super.onStart();
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.settings_api_title);
findPreference(getString(R.string.publicapi_package_key))
.setSummary(BuildConfig.APPLICATION_ID);
}
}
@@ -11,7 +11,6 @@ import androidx.preference.PreferenceFragmentCompat;
import de.dennisguse.opentracks.AbstractActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.databinding.SettingsBinding;
import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
public class SettingsActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
@@ -29,7 +29,7 @@ import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.sensors.BluetoothUtils;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
/**
* Preference to select a discoverable Bluetooth LE device.
@@ -145,20 +145,18 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
bluetoothIcon = AnimatedVectorDrawableCompat.create(getContext(), R.drawable.ic_bluetooth_searching_animated_24dp);
bluetoothIcon.start();
if (!PermissionUtils.hasBluetoothPermissions(getContext())) {
PermissionUtils.requestBluetoothPermission(this,
this::startBluetoothScan,
() -> {
if (PermissionUtils.shouldShowRequestPermissionRationaleBluetooth(this)) {
Toast.makeText(getContext(), R.string.permission_bluetooth_failed_rejected, Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getContext(), R.string.permission_bluetooth_failed, Toast.LENGTH_SHORT).show();
}
dismiss();
});
} else {
startBluetoothScan();
}
PermissionRequester.BLUETOOTH.requestPermissionsIfNeeded(getContext(), this,
this::startBluetoothScan,
(requester) -> {
if (requester.shouldShowRequestPermissionRationale(this)) {
Toast.makeText(getContext(), R.string.permission_bluetooth_failed_rejected, Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getContext(), R.string.permission_bluetooth_failed, Toast.LENGTH_SHORT).show();
}
dismiss();
});
startBluetoothScan();
}
private void startBluetoothScan() {
@@ -5,7 +5,6 @@ import android.content.Context;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.DatePicker;
@@ -49,8 +49,7 @@ public class IntervalStatistics {
interval.add(trackStatisticsUpdater.getTrackStatistics(), trackPoint);
double adjustFactor = distanceInterval.dividedBy(interval.distance);
Interval adjustedInterval = new Interval(interval);
adjustedInterval.adjust(adjustFactor);
Interval adjustedInterval = new Interval(interval, adjustFactor);
intervalList.set(intervalList.size() - 1, adjustedInterval);
@@ -116,7 +115,6 @@ public class IntervalStatistics {
public Interval(Interval i, double adjustFactor) {
distance = i.distance.multipliedBy(adjustFactor);
time = Duration.ofMillis((long) (i.time.toMillis() * adjustFactor));
time = i.time;
gain_m = i.gain_m;
loss_m = i.loss_m;
avgHeartRate = i.avgHeartRate;
@@ -130,11 +128,6 @@ public class IntervalStatistics {
avgHeartRate = i.avgHeartRate;
}
private void adjust(double adjustFactor) {
distance = distance.multipliedBy(adjustFactor);
time = Duration.ofMillis((long) (time.toMillis() * adjustFactor));
}
public Distance getDistance() {
return distance;
}
@@ -21,8 +21,10 @@ import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.util.Log;
import android.util.Pair;
import android.view.Menu;
@@ -55,6 +57,8 @@ public class MarkerEditActivity extends AbstractActivity {
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
private static final String CAMERA_PHOTO_URI_KEY = "camera_photo_uri_key";
private static final String TAG = MarkerEditActivity.class.getSimpleName();
private Track.Id trackId;
private Marker marker;
@@ -86,6 +90,10 @@ public class MarkerEditActivity extends AbstractActivity {
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
Marker.Id markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
if (savedInstanceState != null) {
cameraPhotoUri = Uri.parse(savedInstanceState.getString(CAMERA_PHOTO_URI_KEY, ""));
}
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
// Setup UI elements
@@ -165,6 +173,14 @@ public class MarkerEditActivity extends AbstractActivity {
takePictureFromCamera = null;
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (cameraPhotoUri != null) {
outState.putString(CAMERA_PHOTO_URI_KEY, cameraPhotoUri.toString());
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.marker_edit, menu);
@@ -232,7 +248,13 @@ public class MarkerEditActivity extends AbstractActivity {
}
private void createMarkerWithGalleryImage() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
Intent intent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, 1);
} else {
intent = new Intent(Intent.ACTION_GET_CONTENT);
}
intent.setType("image/*");
takePictureFromGallery.launch(intent);
}
@@ -22,6 +22,8 @@ import android.view.ViewGroup.LayoutParams;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
@@ -169,7 +171,7 @@ public class ListItemUtils {
textView.setVisibility(View.VISIBLE);
textView.setText(value);
if (addShadow) {
textView.setShadowLayer(5, 0, 2, context.getResources().getColor(android.R.color.black));
textView.setShadowLayer(5, 0, 2, ContextCompat.getColor(context, android.R.color.black));
} else {
textView.setShadowLayer(0, 0, 0, 0);
}
@@ -27,11 +27,11 @@ abstract public class RecyclerViewSwipeDeleteCallback extends ItemTouchHelper.Ca
public RecyclerViewSwipeDeleteCallback(Context context) {
background = new ColorDrawable();
backgroundColor = context.getResources().getColor(R.color.colorBackgroundDelete);
backgroundColor = ContextCompat.getColor(context, R.color.colorBackgroundDelete);
clearPaint = new Paint();
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
deleteDrawable = ContextCompat.getDrawable(context, R.drawable.ic_delete_forever_24dp);
deleteDrawable.setTint(context.getResources().getColor(R.color.colorForegroundDelete));
deleteDrawable.setTint(ContextCompat.getColor(context, R.color.colorForegroundDelete));
intrinsicWidth = deleteDrawable.getIntrinsicWidth();
intrinsicHeight = deleteDrawable.getIntrinsicHeight();
}
@@ -65,7 +65,7 @@ public class EGM2008Utils {
if (location.getLongitude() >= 0) {
longitude = location.getLongitude();
} else {
longitude = 180 + Math.abs(location.getLongitude());
longitude = 360 + location.getLongitude();
}
int longitudeIndex = (int) (longitude * RESOLUTION_IN_MINUTES);
@@ -172,5 +172,13 @@ public class EGM2008Utils {
public int hashCode() {
return Objects.hash(latitudeIndex, longitudeIndex);
}
@Override
public String toString() {
return "Indices{" +
"latitudeIndex=" + latitudeIndex +
", longitudeIndex=" + longitudeIndex +
'}';
}
}
}
@@ -17,8 +17,6 @@ import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Track;
@@ -9,6 +9,7 @@ import android.util.Log;
import android.util.Pair;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import java.util.ArrayList;
@@ -138,7 +139,7 @@ public class IntentDashboardUtils {
* @param targetClass the target class
* @param trackIds the track ids
*/
public static void startDashboard(Context context, boolean isRecording, String targetPackage, String targetClass, Track.Id... trackIds) {
public static void startDashboard(Context context, boolean isRecording, @Nullable String targetPackage, @Nullable String targetClass, Track.Id... trackIds) {
if (trackIds.length == 0) {
return;
}
@@ -168,7 +169,7 @@ public class IntentDashboardUtils {
clipData.addItem(new ClipData.Item(uris.get(MARKERS_URI_INDEX)));
intent.setClipData(clipData);
if ((targetPackage != null) && (targetClass != null)) {
if (targetPackage != null && targetClass != null) {
Log.i(TAG, "Starting dashboard activity with explicit intent (package=" + targetPackage + ", class=" + targetClass + ")");
intent.setClassName(targetPackage, targetClass);
} else {
@@ -0,0 +1,101 @@
package de.dennisguse.opentracks.util;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.activity.result.ActivityResultCaller;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class PermissionRequester {
private final List<String> permissions;
public PermissionRequester(List<String> permissions) {
this.permissions = permissions;
}
public boolean hasPermission(Context context) {
return permissions.stream()
.map(p -> ContextCompat.checkSelfPermission(context, p))
.allMatch(r -> r == PackageManager.PERMISSION_GRANTED);
}
public void requestPermissionsIfNeeded(Context context, ActivityResultCaller caller, @Nullable Runnable onGranted, @Nullable RejectedCallback onRejected) {
if (!hasPermission(context)) {
requestPermission(caller, onGranted, onRejected);
}
}
public boolean shouldShowRequestPermissionRationale(Fragment context) {
return permissions.stream()
.anyMatch(context::shouldShowRequestPermissionRationale);
}
private void requestPermission(ActivityResultCaller context, @Nullable Runnable onGranted, @Nullable RejectedCallback onRejected) {
ActivityResultLauncher<String[]> locationPermissionRequest = context.registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
boolean isGranted = permissions.stream()
.allMatch(p -> result.getOrDefault(p, false));
if (isGranted && onGranted != null) {
onGranted.run();
}
if (!isGranted && onRejected != null) {
onRejected.rejected(this);
}
}
);
locationPermissionRequest.launch(permissions.toArray(permissions.toArray(new String[0])));
}
private static final List<String> GPS_PERMISSION = List.of(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION);
private static final List<String> BLUETOOTH_PERMISSIONS;
static {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
BLUETOOTH_PERMISSIONS = List.of(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT);
} else {
BLUETOOTH_PERMISSIONS = Collections.emptyList();
}
}
private static final List<String> NOTIFICATION_PERMISSIONS;
static {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
NOTIFICATION_PERMISSIONS = List.of(Manifest.permission.POST_NOTIFICATIONS);
} else {
NOTIFICATION_PERMISSIONS = Collections.emptyList();
}
}
private static final List<String> ALL_PERMISSIONS;
static {
ArrayList<String> all = new ArrayList<>(GPS_PERMISSION);
all.addAll(BLUETOOTH_PERMISSIONS);
all.addAll(NOTIFICATION_PERMISSIONS);
ALL_PERMISSIONS = Collections.unmodifiableList(all);
}
public final static PermissionRequester GPS = new PermissionRequester(GPS_PERMISSION);
public final static PermissionRequester BLUETOOTH = new PermissionRequester(BLUETOOTH_PERMISSIONS);
public final static PermissionRequester NOTIFICATION = new PermissionRequester(NOTIFICATION_PERMISSIONS);
public final static PermissionRequester ALL = new PermissionRequester(ALL_PERMISSIONS);
public interface RejectedCallback {
void rejected(PermissionRequester permissionRequester);
}
}
@@ -1,74 +0,0 @@
package de.dennisguse.opentracks.util;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.activity.result.ActivityResultCaller;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import java.util.Arrays;
public class PermissionUtils {
private static final String[] GPS = new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION};
private static final String[] BLUETOOTH;
static {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
BLUETOOTH = new String[]{};
} else {
BLUETOOTH = new String[]{Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT};
}
}
private PermissionUtils() {
}
public static boolean hasGPSPermission(Context context) {
return hasPermissions(context, GPS);
}
public static void requestGPSPermission(ActivityResultCaller context, @Nullable Runnable onGranted, @Nullable Runnable onRejected) {
requestPermission(context, GPS, onGranted, onRejected);
}
public static boolean shouldShowRequestPermissionRationaleBluetooth(Fragment context) {
return Arrays.stream(BLUETOOTH).anyMatch(context::shouldShowRequestPermissionRationale);
}
public static boolean hasBluetoothPermissions(Context context) {
return hasPermissions(context, BLUETOOTH);
}
public static void requestBluetoothPermission(ActivityResultCaller context, @Nullable Runnable onGranted, @Nullable Runnable onRejected) {
requestPermission(context, BLUETOOTH, onGranted, onRejected);
}
private static void requestPermission(ActivityResultCaller context, final String[] permissions, @Nullable Runnable onGranted, @Nullable Runnable onRejected) {
ActivityResultLauncher<String[]> locationPermissionRequest = context.registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
boolean isGranted = Arrays.stream(permissions)
.allMatch(p -> result.getOrDefault(p, false));
if (isGranted && onGranted != null) {
onGranted.run();
}
if (!isGranted && onRejected != null) {
onRejected.run();
}
}
);
locationPermissionRequest.launch(permissions);
}
private static boolean hasPermissions(Context context, String[] permissions) {
return Arrays.stream(permissions)
.map(p -> ContextCompat.checkSelfPermission(context, p))
.allMatch(r -> r == PackageManager.PERMISSION_GRANTED);
}
}
@@ -254,13 +254,14 @@ public class StringUtils {
/**
* @return the formatted altitude_m (or null) and it's unit as {@link Pair}
*/
//TODO altitude_m should be double or a value object
public static Pair<String, String> getAltitudeParts(Context context, Float altitude_m, UnitSystem unitSystem) {
DistanceFormatter formatter = DistanceFormatter.Builder()
.setDecimalCount(1)
.setUnit(unitSystem)
.build(context);
Distance distance = altitude_m != null ? Distance.of(altitude_m) : Distance.of(Double.NaN); //TODO Refactor Double.NaN
Distance distance = altitude_m != null ? Distance.of(altitude_m) : Distance.of((Double) null);
return formatter.getDistanceParts(distance);
}
+8 -7
View File
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!--
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2010 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,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.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
--><resources>
<!-- Activity Type -->
<string name="activity_type_airplane">طائرة</string>
<string name="activity_type_atv">ATV</string>
@@ -200,14 +200,15 @@ limitations under the License.
<string name="settings_stats_units_imperial">وحدات القياس البريطانية (ميل، قدم)</string>
<string name="settings_stats_units_metric">وحدات القياس المترية (كم، م)</string>
<string name="settings_stats_units_title">الوحدات المفضلة</string>
<!-- Share Track -->
<string name="share_track_share_file_body">أعتقد أنك قد تكون مهتمًا بهذا المسار.\n\nلعرض هذا المسار، استخدام برنامج Google Earth أو غيره من البرامج تصميم الخرائط.\n\n%1$s</string>
<string name="share_track_share_file_body">أعتقد أنك قد تكون مهتمًا بهذا المسار.
\n
\n%1$s</string>
<string name="share_track_subject">أريد مشاركة مسار معك</string>
<!-- Stats -->
<string name="stats_average_moving_pace">متوسط إيقاع التحرك</string>
<string name="stats_average_moving_speed">متوسط سرعة التحرك</string>
<string name="stats_average_pace">متوسط ​​الإيقاع</string>
<string name="stats_average_pace">متوسط الإيقاع</string>
<string name="stats_average_speed">متوسط السرعة</string>
<string name="stats_altitude">الارتفاع</string>
<string name="stats_fastest_pace">أسرع إيقاع</string>
@@ -352,4 +353,4 @@ limitations under the License.
<string name="waypoint_type_waterfall">شلال</string>
<string name="waypoint_type_water_fountain">نافورة مياه</string>
<string name="waypoint_type_waypoint">نقطة طريق</string>
</resources>
</resources>
-1
View File
@@ -411,7 +411,6 @@
<string name="stats_split_interval">Intervalo partido</string>
<string name="track_recording_notification_accuracy">Precisión de ubicación: %1$s</string>
<string name="track_pace_notification">Ritmo: %1$s</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="track_detail_intervals_tab">Intervalos</string>
<string name="interval_list_empty_message">Aún no hay ningún intervalo</string>
<string name="settings_recording_customize_layout_title">Personalice su pantalla de grabación</string>
-1
View File
@@ -458,7 +458,6 @@ limitations under the License.
<string name="settings_recording_show_on_lockscreen_while_recording_title">Показване на статистически данни на заключения екран</string>
<string name="help_data_google_play_services_title">Изисква ли OpenTracks/използва ли услугата Google Play\?</string>
<string name="about_version_code">Код на версията: %1$d</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="export_error_post_workout">Експортът след тренировка е неуспешен, моля, проверете директорията за експорт.</string>
<string name="about_third_party_data">Данни от трети страни</string>
<string name="import_unsupported_format">Неподдържан формат на файла</string>
+1 -1
View File
@@ -469,7 +469,6 @@ limitations under the License.
<string name="track_speed_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>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="delete_service">Služba mazání</string>
<string name="menu_voice_rate">Rychlost hlasových oznámení</string>
<string name="track_delete_not_recording">Není možno vymazat aktuálně zaznamenávanou trasu.</string>
@@ -574,4 +573,5 @@ limitations under the License.
<string name="settings_public_api_dashboard_enabled_summary_on">Aplikace, která zahájila nahrávání, může také přistupovat k nahraným datům.</string>
<string name="settings_public_api_dashboard_enabled_summary_off">Zaznamenaná data nebudou automaticky sdílena s jinými aplikacemi.</string>
<string name="pace">Tempo</string>
<string name="track_discarding">Mazání trasy</string>
</resources>
-1
View File
@@ -467,7 +467,6 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
<string name="track_speed_notification">Hastighed: %1$s</string>
<string name="about_third_party_data">Data fra tredjeparter</string>
<string name="delete_service">Tjeneste til sletning</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="menu_voice_rate">Stemmehastighed</string>
<string name="track_delete_not_recording">Dette spor kan ikke slettes, da det er optaget i øjeblikket.</string>
<string name="settings_sensor_cycling_distance_speed">Afstand og hastighed</string>
+6 -5
View File
@@ -276,9 +276,8 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka
\n%1$s</string>
<string name="share_track_subject">Ich möchte einen Track mit dir teilen</string>
<!-- Stats -->
<string name="stats_average_moving_pace">Durchschnitt\n(Bewegung)</string>
<string name="stats_average_moving_speed">Durchschnittsgeschwindigkeit
\n(Bewegung)</string>
<string name="stats_average_moving_pace">Durchschnittliches Bewegungstempo</string>
<string name="stats_average_moving_speed">Mittlere Geschwindigkeit in Bewegung</string>
<string name="stats_average_pace">Durchschnitt</string>
<string name="stats_average_speed">Durchschnittsgeschwindigkeit</string>
<string name="stats_altitude">Höhe</string>
@@ -464,13 +463,12 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka
<string name="about_third_party_data">Daten von Drittherstellern</string>
<string name="sensor_state_heart_rate_max">Maximaler Herzfrequenz</string>
<string name="track_delete_not_recording">Dieser Track kann nicht gelöscht werden, da er gerade aufgezeichnet wird.</string>
<string name="interval_list_empty_message">Es gibt noch keine Intervalle.</string>
<string name="interval_list_empty_message">Es gibt noch keine Intervalle</string>
<string name="import_parser_error">Parser Fehler: %1$s</string>
<string name="settings_recording_fullscreen_on_while_recording_summary">Anzeige im Vollbildmodus während der Aufzeichnung.</string>
<string name="sensor_state_heart_rate_avg">Durchschnittlicher Herzfrequenz</string>
<string name="settings_prevent_reimport_tracks_title">Verhindere den erneuten Import von Strecken(aufzeichnungen)</string>
<string name="import_no_kml_file_found">Keine doc.kml Datei im KMZ gefunden</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="import_thread_interrupted">Thread unterbrochen</string>
<string name="import_unable_to_import_file">Importieren der Datei gescheitert: %1$s</string>
<string name="delete_service">Löschdienst</string>
@@ -575,4 +573,7 @@ Meldet das GPS-Gerät ungenaue Daten (z.B. Standort, Geschwindigkeit, Höhe), ka
<string name="report_bug">Fehler melden</string>
<string name="settings_ui_summary">Erscheinungsbild, Aufzeichnungslayout, Theme</string>
<string name="settings_gps_summary">Zeitintervall, Entfernungsintervall, Max Entfernung, Genauigkeit</string>
<string name="description_speed_nautical">Geschwindigkeit (Knoten)</string>
<string name="settings_open_tracks_title">Open Tracks</string>
<string name="gps_recording_status">%1$s: %2$s</string>
</resources>
-1
View File
@@ -464,7 +464,6 @@ limitations under the License.
<string name="track_pace_notification">Ritmo: %1$s</string>
<string name="about_third_party_data">Datos de terceros</string>
<string name="delete_service">Borrar servicio</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="menu_voice_rate">Velocidad de la voz</string>
<string name="track_delete_not_recording">Este recorrido no puede ser eliminado porque se está grabando actualmente.</string>
<string name="settings_sensor_running_speed_and_cadence">Distancia, velocidad y cadencia</string>
-1
View File
@@ -464,7 +464,6 @@ limitations under the License.
<string name="track_pace_notification">Tempo: %1$s</string>
<string name="about_third_party_data">Kolmanda osapoole andmed</string>
<string name="delete_service">Kustutamise teenus</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="menu_voice_rate">Hääle kiirus</string>
<string name="track_delete_not_recording">Seda pala ei saa kustutada, kuna see on praegu salvestatud.</string>
<string name="settings_sensor_cycling_cadence">Rütm</string>
-1
View File
@@ -467,7 +467,6 @@ GPS gailuak datu okerrak salatzen baditu (adibidez, kokapena, abiadura, kota), O
<string name="track_pace_notification">Erritmoa: %1$s</string>
<string name="about_third_party_data">Hirugarrenen datuak</string>
<string name="delete_service">Ezabatzeko zerbitzua</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="menu_voice_rate">Ahots abiadura</string>
<string name="track_delete_not_recording">Pista hau ezin da ezabatu da gaur egun grabatzen.</string>
<string name="settings_sensor_cycling_cadence">Kadentzia</string>
-1
View File
@@ -469,7 +469,6 @@ limitations under the License.
<string name="track_speed_notification">Nopeus: %1$s</string>
<string name="about_third_party_data">Kolmannen osapuolen tiedot</string>
<string name="delete_service">Poistopalvelu</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="menu_voice_rate">Äänen nopeus</string>
<string name="track_delete_not_recording">Tätä reittiä ei voi poistaa koska sitä tallennetaan parhaillaan.</string>
<string name="settings_sensor_cycling_cadence">Pyöräily: tahtiaika</string>
+19 -1
View File
@@ -465,7 +465,6 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
<string name="export_track_already_exists_msg">La trace %1$s existe déjà dans le répertoire de destination.</string>
<string name="export_progress_summary_overwrite_msg">Traces écrasées</string>
<string name="about_third_party_data">Données de tiers</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="delete_service">Service de suppression</string>
<string name="menu_voice_rate">Vitesse vocale</string>
<string name="track_delete_not_recording">Cette piste ne peut pas être supprimée car elle est en cours d\'enregistrement.</string>
@@ -587,4 +586,23 @@ Si le dispositif GPS signale des données inexactes (par exemple, la localisatio
<string name="settings_announcements_lap_heart_rate">Fréquence cardiaque au tour</string>
<string name="image_discard">Ignorer</string>
<string name="track_discarding">Ignorer la trace</string>
<string name="description_pace_nautical">Allure (min/MN)</string>
<string name="show_on_dashboard_not_installed">Il n\'y a pas d\'application installé supportant l\'API OpenTracks Dashboard.</string>
<string name="description_speed_nautical">Vitesse (nœuds)</string>
<string name="settings_stats_units_nautical">Nautique (MN, pieds)</string>
<string name="unit_nautical_mile">MN</string>
<string name="unit_knots">kn</string>
<string name="unit_minute_per_nautical_mile">min/MN</string>
<string name="value_integer_knots">%1$d nœuds</string>
<string name="value_float_knots">%1$.1f nœuds</string>
<string name="value_float_knots_recommended">%1$.1f nœuds (recommandé)</string>
<string name="voice_per_nautical_mile">par mile nautique</string>
<plurals name="voiceSpeedMKnots">
<item quantity="one">%1$.1f nœud</item>
<item quantity="other">%1$.1f nœuds</item>
</plurals>
<plurals name="voiceDistanceNauticalMiles">
<item quantity="one">%1$.2f mile nautique</item>
<item quantity="other">%1$.2f milles nautique</item>
</plurals>
</resources>
+32 -1
View File
@@ -472,7 +472,6 @@ Se te detés ou estás en interiores, non se gardarán datos do sensor (mais ser
<string name="track_speed_notification">Velocidade: %1$s</string>
<string name="about_third_party_data">Datos de terceiros alleos</string>
<string name="delete_service">Eliminación de servizo</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="menu_voice_rate">Velocidade da voz</string>
<string name="track_delete_not_recording">Esta pista non se pode eliminar porque está a funcionar a gravación.</string>
<string name="settings_sensor_cycling_distance_speed">Distancia e Velocidade</string>
@@ -580,4 +579,36 @@ Se te detés ou estás en interiores, non se gardarán datos do sensor (mais ser
<string name="gps_recording_status">%1$s: %2$s</string>
<string name="gps_recording_without_signal">OpenTracks está gravando sen datos GPS.</string>
<string name="settings_export_filename_title">Formato do nome do ficheiro</string>
<string name="settings_announcements_lap_heart_rate">Ritmo cardíaco na volta</string>
<string name="settings_announcements_average_heart_rate">Ritmo cardíaco medio</string>
<string name="image_discard">Descartar</string>
<string name="settings_announcements_average_speed_pace">Velocidade/ritmo medio</string>
<string name="settings_announcements_lap_speed_pace">Velocidade/ritmo da volta</string>
<string name="track_discarding">Desbotando Ruta</string>
<string name="share_crash_report">Comparte o informe do fallo</string>
<string name="description_pace_nautical">Ritmo (min/NM)</string>
<string name="description_speed_nautical">Velocidade (nós)</string>
<string name="settings_stats_units_nautical">Náutica (NM, ft)</string>
<string name="unit_nautical_mile">NM</string>
<string name="unit_knots">kn</string>
<string name="unit_minute_per_nautical_mile">min/NM</string>
<string name="value_integer_knots">%1$d nós</string>
<string name="value_float_knots">%1$.1f nós</string>
<string name="value_float_knots_recommended">%1$.1f nós (recomendado)</string>
<string name="voice_per_nautical_mile">por milla náutica</string>
<plurals name="voiceDistanceNauticalMiles">
<item quantity="one">1 milla náutica</item>
<item quantity="other">%1$.2f millas náuticas</item>
</plurals>
<string name="show_on_dashboard">Taboleiro da API OpenTracks</string>
<plurals name="voiceSpeedMKnots">
<item quantity="one">1 nó</item>
<item quantity="other">%1$.1f nós</item>
</plurals>
<string name="show_on_dashboard_not_installed">Non hai ningunha aplicación instalada con soporte para a API do taboleiro OpenTracks.</string>
<string name="select_show_on_map_behavior">Elixe \"ver no mapa\"</string>
<string name="show_on_map_format_ask">Preguntar</string>
<string name="always">Sempre</string>
<string name="error_crash_title">%1$s fallou</string>
<string name="report_bug">Informar do problema</string>
</resources>
-1
View File
@@ -460,7 +460,6 @@ limitations under the License.
\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>
<string name="delete_service">Törlési szolgáltatás</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="menu_voice_rate">Hang sebesség</string>
<string name="track_delete_not_recording">Ez a sáv nem törölhető, mivel jelenleg is rögzítve van.</string>
<string name="settings_sensor_cycling_cadence">Kerékpározás: lépésszám</string>
-1
View File
@@ -471,7 +471,6 @@ Infatti, un\'applicazione deve supportare <i>ACTION_VIEW</i> con MIME <i>applica
<string name="track_pace_notification">Ritmo: %1$s</string>
<string name="about_third_party_data">Dati di terzi</string>
<string name="delete_service">Servizio di eliminazione</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="menu_voice_rate">Velocità della voce</string>
<string name="track_delete_not_recording">Questa traccia non può essere cancellata perché è attualmente in registrazione.</string>
<string name="settings_sensor_cycling_cadence">Cadenza</string>
-1
View File
@@ -416,7 +416,6 @@ limitations under the License.
<string name="help_recording_content">OpenTracks naudoja prietaiso GPS jutiklį, kad nuolat įrašytų vietą. Jei GPS įrenginys praneša apie netikslius duomenis (pvz., Vietą, greitį, aukštį), OpenTracks negali nieko padaryti.</string>
<string name="generic_open_track">Atidaryti takelį</string>
<string name="settings_recording_keep_screen_on_while_recording_summary">Įrašydami palaikykite ekraną įjungtą.</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="open_track_as_GPX">Atidaryti kaip GPX</string>
<string name="help_gps_troubleshooting_title">OpenTracks nefiksuoja vietų\?</string>
<string name="generic_overwrite">Perrašyti</string>
-1
View File
@@ -416,7 +416,6 @@ limitations under the License.
<string name="help_track_export_title">Kā es varu eksportēt/ kopīgot ierakstītos ierakstus\?</string>
<string name="instant_export_enabled_summary">Dziesmas eksportēšana uz krātuvi pēc ierakstīšanas pabeigšanas</string>
<string name="activity_type_escooter">eScooter</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="sensor_state_power_avg">Vidējā jauda</string>
<string name="about_libraries">Bibliotēkas</string>
<string name="description_default_speed_or_pace">Pēc aktivitātes veida</string>
+1 -1
View File
@@ -460,7 +460,6 @@ limitations under the License.
<string name="track_pace_notification">Tempo: %1$s</string>
<string name="about_third_party_data">Tredjepartsdata</string>
<string name="delete_service">Slettingstjeneste</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="menu_voice_rate">Talehastighet</string>
<string name="track_delete_not_recording">Dette sporet kan ikke slettes da det tas opp nå.</string>
<string name="settings_sensor_cycling_cadence">Kadens</string>
@@ -599,4 +598,5 @@ limitations under the License.
<item quantity="one">1 nautisk mil</item>
<item quantity="other">%1$.2f nautiske mil</item>
</plurals>
<string name="show_on_dashboard_not_installed">Ingen app som støtter OpenTracks Dashboard API er installert.</string>
</resources>
-1
View File
@@ -465,7 +465,6 @@ Als het GPS-apparaat onnauwkeurige gegevens rapporteert (bijv. locatie, snelheid
<string name="track_distance_notification">Afstand: %1$s</string>
<string name="track_pace_notification">Tempo: %1$s</string>
<string name="about_third_party_data">Gegevens van derden</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="delete_service">Dienst verwijdering</string>
<string name="menu_voice_rate">Spraak snelheid</string>
<string name="track_delete_not_recording">Dit fragment kan niet worden gewist omdat het momenteel is opgenomen.</string>
-1
View File
@@ -476,7 +476,6 @@ limitations under the License.
<string name="track_distance_notification">Odległość: %1$s</string>
<string name="about_third_party_data">Dane osób trzecich</string>
<string name="delete_service">Usługa usuwania</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="menu_voice_rate">Szybkość głosu</string>
<string name="track_delete_not_recording">Tej ścieżki nie można usunąć, ponieważ jest ona aktualnie nagrywana.</string>
<string name="settings_sensor_cycling_distance_speed">Dystans i Prędkość</string>
-1
View File
@@ -404,7 +404,6 @@
<string name="track_pace_notification">Parcela: %1$s</string>
<string name="about_third_party_data">Dados de terceiros</string>
<string name="delete_service">Serviço de exclusão</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="menu_voice_rate">Velocidade da voz</string>
<string name="track_delete_not_recording">Esta trilha não pode ser excluída porque está gravada no momento.</string>
<plurals name="voiceSpeedKilometersPerHour">
-1
View File
@@ -426,7 +426,6 @@ limitations under the License.
<string name="import_progress_summary_exists_msg">Există deja</string>
<string name="track_detail_intervals_tab">Intervaluri</string>
<string name="track_recording_notification_accuracy">Precizia locației: %1$s</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="sensor_state_cadence_max">Max Cadență</string>
<string name="sensor_could_not_scan">Nu a fost posibilă scanarea pentru dispozitive Bluetooth (eroare %1$i).</string>
<string name="share_image_subject">Aș dori să vă împărtășesc o imagine</string>
+66 -38
View File
@@ -24,7 +24,7 @@ limitations under the License.
<string name="activity_type_commercial_airplane">Пассажирские самолеты</string>
<string name="activity_type_cross_country_skiing">лыжные гонки</string>
<string name="activity_type_cycling">Езда на велосипеде</string>
<string name="activity_type_dirt_bike">Езда на велосипеде по бездорожью</string>
<string name="activity_type_dirt_bike">Дертбайк</string>
<string name="activity_type_donkey_back_riding">Верховая езда на ослах</string>
<string name="activity_type_driving">на автомобиле</string>
<string name="activity_type_driving_bus">Вождение автобуса</string>
@@ -54,7 +54,7 @@ limitations under the License.
<string name="activity_type_rc_boat">Радиоуправляемые лодки</string>
<string name="activity_type_rc_helicopter">Радиоуправляемые вертолеты</string>
<string name="activity_type_riding">Верховая езда</string>
<string name="activity_type_road_biking">Езда на шоссейном велосипеде</string>
<string name="activity_type_road_biking">Шоссейный велосипед</string>
<string name="activity_type_roller_skiing">роликовые лыжи</string>
<string name="activity_type_rowing">Гребля академическая</string>
<string name="activity_type_running">Бег</string>
@@ -128,7 +128,7 @@ limitations under the License.
<string name="image_arrow">Стрелка</string>
<string name="image_marker">Маркер</string>
<string name="image_marker_count">Количество маркеров</string>
<string name="image_photo_marker">Маркер фотографии</string>
<string name="image_photo_marker">Фотомаркер</string>
<string name="image_record">Запись</string>
<string name="image_stop">Стоп</string>
<string name="image_track">Трек</string>
@@ -212,7 +212,7 @@ limitations under the License.
<string name="stats_average_speed">Средняя скорость</string>
<string name="stats_altitude">Высота</string>
<string name="stats_fastest_pace">Макс. темп</string>
<string name="stats_gain">Рост</string>
<string name="stats_gain">Набор высоты</string>
<string name="stats_latitude">Широта</string>
<string name="stats_longitude">Долгота</string>
<string name="stats_max_speed">Макс. скорость</string>
@@ -337,53 +337,53 @@ limitations under the License.
<string name="waypoint_type_telephone">Телефон</string>
<string name="waypoint_type_theater">Театр</string>
<string name="waypoint_type_toilet">Туалет</string>
<string name="waypoint_type_tourist_information">Туристическая информация</string>
<string name="waypoint_type_tourist_information">Информация для туристов</string>
<string name="waypoint_type_tree">Дерево</string>
<string name="waypoint_type_tunnel">Тоннель</string>
<string name="waypoint_type_waterfall">Водопад</string>
<string name="waypoint_type_water_fountain">Фонтан</string>
<string name="waypoint_type_waypoint">путевая точка</string>
<string name="help_track_map_content"> Используйте значок маленькой карты.
<string name="help_track_map_content"> Используйте маленький значок карты.
\na) (живые обновления) Установите <a href="https://github.com/OpenTracksApp/OSMDashboard">OSMDashboard</a>. Он предоставляет карты в режиме онлайн или оффлайн с помощью <a href="https://www.openstreetmap.org">OpenStreetMap</a>.
\nНа самом деле, OSMDashboard - это отдельное приложение, которое может получить доступ (временно) только к данным треков, которые будут показаны.
\nНа самом деле, OSMDashboard — это отдельное приложение, которое может получить доступ (временно) только к данным треков, которые будут показаны.
\nb) Через обычное картографическое приложение (например, <a href="http://osmand.net/">OsmAnd</a> и <a href="https://maps.me/">MAPS.ME</a>). Эти приложения должны поддерживать формат файлов KMZ (только местоположения и метки времени). Фактически, приложение должно поддерживать <i>ACTION_VIEW</i> с MIME <i>application/vnd.google-earth.kmz</i>. </string>
<string name="help_storage_pictures_title">Как OpenTracks хранит изображения, сделанные в качестве маркеров\?</string>
<string name="about_preference_title">О OpenTracks</string>
<string name="about_description"><i>OpenTracks</i> - это приложение для отслеживания спортивных событий, которое полностью уважает вашу конфиденциальность.</string>
<string name="about_preference_title">Об OpenTracks</string>
<string name="about_description"><i>OpenTracks</i> — это приложение для отслеживания спортивных событий, которое полностью уважает вашу конфиденциальность.</string>
<string name="about_url">Веб-страница проекта: <a href="%1$s">%1$s</a></string>
<string name="about_version_name">Название версии: %1$s</string>
<string name="about_version_code">Код версии: %1$d</string>
<string name="about_history">OpenTracks - это форк <a href="https://code.google.com/archive/p/mytracks/"><i>Google MyTracks</i></a> (разработка прекращена в 2016 г.).</string>
<string name="about_history">OpenTracks — это форк <a href="https://code.google.com/archive/p/mytracks/"><i>Google MyTracks</i></a> (разработка прекращена в 2016 г.).</string>
<string name="about_libraries">Библиотеки</string>
<string name="help_feature_title">OpenTracks имеет следующие возможности:</string>
<string name="help_feature_content">* создание GPS-треков
\n* использование датчика частоты пульса (только Bluetooth LE)
\n* голосовые объявления (по времени или расстоянию)
\n* создание маркеров (в т.ч. картинки)
\n* голосовые повещения (по времени или расстоянию)
\n* создание маркеров (в т.ч. с изображениями)
\n* работа полностью офлайн
\n* показ треков на карте (см. ниже)</string>
<string name="help_track_map_title">Как показать трек на карте\?</string>
<string name="help_storage_pictures_content">Они хранятся во внутренней памяти приложения (т. е. доступны только в OpenTracks).</string>
<string name="help_track_export_title">Как я могу экспортировать/поделиться записанными треками\?</string>
<string name="help_data_google_play_services_title">Требуется/используется ли OpenTracks сервисы Google Play\?</string>
<string name="help_data_google_play_services_title">Требует/использует ли OpenTracks сервисы Google Play\?</string>
<string name="help_recording_title">Как OpenTracks записывает данные\?</string>
<string name="help_track_export_content"> Трек может быть экспортирован из \"Настроек\" в виде KMZ, KML или GPX. KMZ/KML все данные; GPX только местоположения. Поделиться треком можно через значок <i>поделиться</i> предоставляется как KMZ. </string>
<string name="help_track_export_content"> Трек может быть экспортирован из \"Настроек\" в виде KMZ, KML или GPX. KMZ/KML все данные; GPX только местоположения. Поделиться треком можно через значок <i>поделиться</i> в формате KMZ. </string>
<string name="help_recording_content">OpenTracks использует датчик GPS устройства для непрерывной записи местоположения. Если устройство GPS сообщает неточные данные (например, местоположение, скорость, высоту), OpenTracks ничего не может с этим поделать.</string>
<string name="export_progress_summary_new_msg">Новые экспортируемые треки</string>
<string name="export_progress_summary_new_msg">Новых треков экспортировано</string>
<string name="help_data_storage_title">Как OpenTracks записывает данные с датчиков\?</string>
<string name="export_track_already_exists_msg">Дорожка %1$s уже существует в каталоге назначения.</string>
<string name="export_without_photos">без фотографий</string>
<string name="export_with_photos">с фотографией</string>
<string name="description_default_speed_or_pace">По типу деятельности</string>
<string name="export_all_do_it_for_all">делать это во всех конфликтах</string>
<string name="stats_loss">Потери</string>
<string name="export_progress_summary_skip_msg">Пропущенные дорожки</string>
<string name="generic_click_twice_cancel">Нажмите назад, чтобы отменить</string>
<string name="export_all_do_it_for_all">для всех конфликтов</string>
<string name="stats_loss">Спуск</string>
<string name="export_progress_summary_skip_msg">Пропущенные треки</string>
<string name="generic_click_twice_cancel">Нажмите назад еще раз, чтобы отменить</string>
<string name="generic_completed">Завершено</string>
<string name="generic_show_errors">Показать ошибки</string>
<string name="generic_separator_done_total">/</string>
<string name="generic_skip">Пропустить</string>
<string name="generic_open_track">Открытая трасса</string>
<string name="generic_open_track">Открыть трек</string>
<string name="export_progress_summary_errors_msg">Ошибки</string>
<string name="import_file_imported">Файл %1$s импортирован</string>
<string name="import_error_list_dialog_title">Файлы не импортированы</string>
@@ -405,7 +405,7 @@ limitations under the License.
<string name="open_track_as_KML">Открыть как KML</string>
<string name="help_support_title">Как поддержать OpenTracks\?</string>
<string name="help_gps_troubleshooting_title">OpenTracks не регистрирует местоположения\?</string>
<string name="activity_type_escooter">эскулап</string>
<string name="activity_type_escooter">Электросамокат</string>
<string name="permission_gps_failed">OpenTracks требует разрешения на использование GPS.</string>
<string name="settings_prevent_reimport_tracks_title">Предотвращение повторного импорта треков</string>
<string name="sensor_state_cadence_max">Макс. каденция</string>
@@ -420,33 +420,33 @@ limitations under the License.
<string name="bluetooth_disabled">Включите Bluetooth.</string>
<string name="sensor_state_power_avg">Средняя мощность</string>
<string name="track_recording_notification_accuracy">Точность расположения: %1$s</string>
<string name="stats_split_interval">Раздельный интервал</string>
<string name="stats_split_interval">Размер интервалов</string>
<string name="settings_sensor_wheel_circumference">Окружность колеса (мм)</string>
<string name="settings_night_mode_option_night">Ночная</string>
<string name="settings_night_mode_option_day">Дневная</string>
<string name="export_progress_summary_overwrite_msg">Перезапись дорожек</string>
<string name="export_track_errors">Треки не экспортируются</string>
<string name="export_progress_summary_overwrite_msg">Перезапись треков</string>
<string name="export_track_errors">Треки не экспортированы</string>
<string name="export_error_post_workout">Сбой экспорта после тренировки, пожалуйста, проверьте каталог экспорта.</string>
<string name="description_altitude_loss">Потеря высоты: %1$d м (%2$d футов)</string>
<string name="settings_default_export_uri_title">Каталог экспорта треков</string>
<string name="instant_export_enabled_summary">Экспорт дорожки в хранилище после завершения записи</string>
<string name="instant_export_enabled_summary">Экспортировать трек в хранилище после завершения записи</string>
<string name="share_image_subject">Я хотел бы поделиться с вами изображением</string>
<string name="track_detail_intervals_tab">Интервалы</string>
<string name="help_data_storage_content"> Данные датчика хранятся для каждой записи. Для датчика пульса это означает, что для каждой записи сохраняется только <i>одно</i> измерение пульса. Если вы стоите на месте или находитесь в помещении, данные датчика не будут записаны (но будут отображаться). </string>
<string name="help_gps_troubleshooting_content"> Что вы можете сделать:
<string name="help_data_storage_content"> Данные датчиков сохраняются для каждого местоположения. Для датчика пульса это означает, что для местоположения сохраняется только <i>одно</i> значение пульса. Если вы стоите на месте или находитесь в помещении, данные датчика не будут записываться (но будут отображаться). </string>
<string name="help_gps_troubleshooting_content"> Что можно сделать:
\n a) проверить, включен ли GPS,
\n б) улучшить прием GPS (например, выехать на природу),
\n б) улучшить прием GPS (например, выйти из помещения),
\n c) проверить с помощью приложения <a href="https://f-droid.org/en/packages/com.android.gpstest.osmdroid/">GPSTest</a>, и
\n г) отключите энергосбережение.
\n г) отключть энергосбережение.
\n
\n Если все это не поможет, пожалуйста, сообщите об ошибке. </string>
<string name="track_pace_notification">Темп: %1$s</string>
<string name="track_speed_notification">Скорость: %1$s</string>
<string name="track_distance_notification">Расстояние: %1$s</string>
<string name="interval_list_empty_message">Интервалов пока нет</string>
<string name="aggregated_stats_empty_message">Запишите свой первый трек, чтобы увидеть агрегированную статистику</string>
<string name="aggregated_stats_empty_message">Запишите свой первый трек, чтобы увидеть собранную статистику</string>
<string name="instant_export_enabled_title">Мгновенный экспорт после тренировки</string>
<string name="hold_to_stop">Удерживать для остановки</string>
<string name="hold_to_stop">Удерживайте для остановки</string>
<string name="share_image_body">Я думаю, вас может заинтересовать это изображение.</string>
<string name="help_support_content"> Вы можете:
\n a) распространять информацию об OpenTracks,
@@ -463,12 +463,11 @@ limitations under the License.
<string name="sensor_could_not_scan">Не удалось выполнить сканирование устройств Bluetooth (ошибка %1$i).</string>
<string name="import_progress_summary_errors_msg">Ошибки</string>
<string name="generic_overwrite">Перезаписать</string>
<string name="import_prevent_reimport">Вы настроили предотвращение повторного импорта трека</string>
<string name="import_prevent_reimport">Вы запретили повторный импорт трека</string>
<string name="delete_service">Услуга удаления</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="about_third_party_data">Сторонние данные</string>
<string name="menu_voice_rate">Скорость голоса</string>
<string name="track_delete_not_recording">Эта дорожка не может быть удалена, поскольку она записана в данный момент.</string>
<string name="track_delete_not_recording">Этот трек нельзя удалить, так как он записывается в данный момент.</string>
<plurals name="generic_completed_with_errors">
<item quantity="one">Завершена с %1$d ошибкой</item>
<item quantity="few">Завершено с %1$d ошибками</item>
@@ -478,7 +477,7 @@ limitations under the License.
<string name="settings_sensor_cycling_cadence">Каденция</string>
<string name="settings_sensor_cycling_distance_speed">Дистанция и Скорость</string>
<string name="settings_sensor_running_speed_and_cadence">Дистанция, Скорость и Каденция</string>
<string name="stats_sensors_cadence">Каденция</string>
<string name="stats_sensors_cadence">Каденс</string>
<string name="activity_type_swimming">Плавание</string>
<string name="activity_type_swimming_open">Плавание на открытой воде</string>
<string name="activity_type_workout">Тренировка</string>
@@ -523,7 +522,7 @@ limitations under the License.
</plurals>
<string name="settings_layout_reset">Сбросить свои раскладки</string>
<string name="settings_layout_reset_done">Настройки всех раскладок были сброшены</string>
<string name="generic_to">До</string>
<string name="generic_to">по</string>
<string name="generic_today">Сегодня</string>
<string name="generic_yesterday">Вчера</string>
<string name="settings_running_sensor">Бег</string>
@@ -537,7 +536,7 @@ limitations under the License.
<string name="settings_layout_reset_confirm_title">Сбросить свои раскладки\?</string>
<string name="settings_layout_reset_confirm_message">Настройки всех раскладок будут сброшены на значения по умолчанию. Это не удалит треки на устройстве.</string>
<string name="stats_sensors_heart_rate">Пульс</string>
<string name="stats_sensors_power">Энергия</string>
<string name="stats_sensors_power">Мощность</string>
<string name="value_float_mile_hour">%1$.1f миль/ч</string>
<string name="voice_per_kilometer">за километр</string>
<string name="voice_per_mile">за милю</string>
@@ -555,7 +554,7 @@ limitations under the License.
<string name="average_heart_rate">Средний пульс</string>
<string name="current_heart_rate">Текущий пульс</string>
<string name="generic_filter">Фильтр</string>
<string name="generic_from">От</string>
<string name="generic_from">с</string>
<string name="settings_export_section">Экспорт</string>
<string name="settings_import_section">Импорт</string>
<string name="settings_api_title">Публичный API</string>
@@ -587,4 +586,33 @@ limitations under the License.
<string name="error_crash_title">%1$s вылетел</string>
<string name="share_crash_report">Поделиться отчётом о вылете</string>
<string name="report_bug">Сообщить об ошибке</string>
<plurals name="voiceSpeedMKnots">
<item quantity="one">%1$.1f узел</item>
<item quantity="few">%1$.1f узла</item>
<item quantity="many">%1$.1f узлов</item>
<item quantity="other">%1$.1f узлов</item>
</plurals>
<string name="unit_knots">уз</string>
<plurals name="voiceDistanceNauticalMiles">
<item quantity="one">%1$.2f морская миля</item>
<item quantity="few">%1$.2f морские мили</item>
<item quantity="many">%1$.2f морских миль</item>
<item quantity="other">%1$.2f морских миль</item>
</plurals>
<string name="value_integer_knots">%1$d узлов</string>
<string name="value_float_knots">%1$.1f узла</string>
<string name="image_discard">Отменить</string>
<string name="track_discarding">Отмена записи трека</string>
<string name="settings_announcements_lap_heart_rate">Значение пульса за круг</string>
<string name="settings_announcements_lap_speed_pace">Скорость/темп за круг</string>
<string name="settings_public_api_package_title">Имя пакета для публичного API</string>
<string name="description_speed_nautical">Скорость (узлы)</string>
<string name="value_float_knots_recommended">%1$.1f узла (рекомендуется)</string>
<string name="settings_announcements_average_heart_rate">Средний пульс</string>
<string name="settings_stats_units_nautical">Морская (морские мили, фут)</string>
<string name="select_show_on_map_behavior">Выберите формат данных для показа на карте</string>
<string name="show_on_map_format_ask">Спрашивать</string>
<string name="always">Запомнить</string>
<string name="voice_per_nautical_mile">за морскую милю</string>
<string name="settings_announcements_average_speed_pace">Средняя скорость/темп</string>
</resources>
-1
View File
@@ -517,7 +517,6 @@ limitations under the License.
<string name="settings_sensors_title">Senzory Bluetooth</string>
<string name="settings_sensors_summary">Cyklistické snímače, obvod kolies, snímače behu</string>
<string name="settings_open_tracks_summary">Viac o projekte OpenTracks</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="settings_gps_summary">Interval času, interval vzdialenosti, Max. vzdialenosť, Presnosť</string>
<string name="help_support_content"/>
<string name="share_image_body">Myslím, že by vás mohol zaujímať tento obrázok.</string>
-1
View File
@@ -488,7 +488,6 @@ limitations under the License.
<string name="lap_time">Čas kroga</string>
<string name="track_detail_intervals_tab">Intervali</string>
<string name="track_delete_not_recording">Te sledi ni mogoče izbrisati, saj je trenutno posneta.</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="stats_clock">Ura</string>
<string name="settings_import_section">Uvoz</string>
<string name="share_image_subject">Rad bi z vami delil sliko</string>
+24
View File
@@ -336,4 +336,28 @@ limitations under the License.
<string name="waypoint_type_waterfall">водопад</string>
<string name="waypoint_type_water_fountain">фонтана</string>
<string name="waypoint_type_waypoint">тачка пута</string>
<string name="about_url">Web stranica projekta: <a href="%1$s">%1$s</a></string>
<string name="help_feature_title">OtvorenaPraćenja ima sledeće funkcije:</string>
<string name="help_storage_pictures_title">Kako OtvorenaPraćenja čuva fotografije tamo gde su postavljeni markeri\?</string>
<string name="about_history">OtvorenaPraćenja je kopija od <a href="https://code.google.com/archive/p/mytracks/"><i>Guglove MojaPraćenja aplikacije</i></a> (zaustavljene u 2016).</string>
<string name="help_track_map_title">Kako da se pokaže praćenja na mapi\?</string>
<string name="help_track_map_content"> Koristi ikonicu male mape.
\nA) (apdejti uživo) Instalirajte
\n<a href="https://github.com/OpenTracksApp/OSMDashboard">OSMPočetna</a>. Izvodi onlajn mape ili oflajn preko <a href="https://www.openstreetmap.org">OtvorenihMapaZaUlice</a>.
\nZapravo, OSMPočtna je pojedina aplikacija koja može pristupiti (privremeno) podacima o praćenjima koji se prikazuju.
\nB) Sa normalnim aplikacijama za mape (npr. <a href="http://osmand.net/">OSMAnd</a> i <a href="https://maps.me/">MOJE.MAPE</a>). Te aplikacije imaju podršku za KMZ fajl format (lokacije i tajmstampovi). U temi, aplikacija mora da podrži <i>ACTION_VIEW</i> sa MIME-om <i>application/vnd.google-earth.kmz</i>. </string>
<string name="about_version_code">Kod verzije: %1$d</string>
<string name="about_libraries">Biblioteke</string>
<string name="about_version_name">Ime verzije: %1$s</string>
<string name="help_feature_content">* pravi praćenja o lokaciji
\n* koristi senzor za merenje srca (samo Blutut LE)
\n* predstavljanje glasa (od vremena ili distance)
\n* pravi markere (uključujući slike)
\n* radi potpuno bez interneta
\n* pokazuje praćenja na mapi (vidi dole)</string>
<string name="about_description"><i>OtvorenaPraćenja</i> je aplikacija za praćenje sporta koja potpuno pazi o vašoj privatnosti.</string>
<string name="about_third_party_data">Informacije od trećih strana</string>
<string name="help_track_export_title">Kako ja mogu da izvezem/podelim sačuvand podatke\?</string>
<string name="about_preference_title">O OtvorenimPraćenjima</string>
<string name="help_storage_pictures_content">Oni su sačuvani u aplikaciji u internoj memoriji (samo pristupnoj bez OtvorenihPraćenja).</string>
</resources>
-1
View File
@@ -431,7 +431,6 @@ limitations under the License.
<string name="generic_overwrite">Skriva</string>
<string name="generic_separator_done_total">/</string>
<string name="export_track_errors">Spår som inte exporteras</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="help_data_storage_content"> Sensordata lagras per inspelad plats. När det gäller en pulssensor betyder det att endast <i>en</i> pulsmätning lagras per plats. Om du står stilla eller är inomhus, kommer inga sensordata att registreras (men det kommer att visas). </string>
<string name="help_recording_title">Hur registrerar OpenTracks data\?</string>
<string name="about_description"><i>OpenTracks</i> är ett program för sportspårning som helt respekterar din integritet.</string>
+1 -1
View File
@@ -492,7 +492,6 @@ limitations under the License.
<string name="settings_prevent_reimport_tracks_title">Parçaların yeniden içe aktarılmasını önleme</string>
<string name="stats_coordinates">Enlem/Boylam</string>
<string name="stats_sensors_power">Güç</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="aggregated_stats_filter_no_results">Belirtilen filtre için etkinlik yok</string>
<string name="value_float_kilometer_hour">%1$.1f km/s</string>
<string name="custom_layout_list_title">Dizilimler</string>
@@ -590,4 +589,5 @@ limitations under the License.
<item quantity="one">1 deniz mili</item>
<item quantity="other">%1$.2f deniz mili</item>
</plurals>
<string name="image_discard"></string>
</resources>
-1
View File
@@ -472,7 +472,6 @@ limitations under the License.
<string name="track_pace_notification">Темп: %1$s</string>
<string name="about_third_party_data">Дані третіх сторін</string>
<string name="delete_service">Служба видалення</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="track_delete_not_recording">Цей трек не може бути видалений в тому вигляді, в якому він записаний в даний момент.</string>
<string name="menu_voice_rate">Швидкість передачі голосу</string>
<string name="settings_sensor_cycling_cadence">Велоспорт: частота обертання педалей</string>
+32 -1
View File
@@ -471,7 +471,6 @@ limitations under the License.
<string name="interval_list_empty_message">Không có bất kỳ khoảng thời gian sơ</string>
<string name="aggregated_stats_empty_message">Hồ sơ của đua đầu tiên, để xem tổng thống kê</string>
<string name="track_detail_intervals_tab">Khoảng thời gian</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="settings_recording_keep_screen_on_while_recording_title">Giữ trên màn hình</string>
<string name="bluetooth_disabled">Xin vui lòng cho phép Bluetooth.</string>
<string name="sensor_state_cadence_avg">Nhịp trung bình</string>
@@ -559,4 +558,36 @@ limitations under the License.
<string name="settings_public_api_dashboard_enabled_summary_on">Một ứng dụng bắt đầu ghi hình cũng có thể truy cập vào dữ liệu ghi lại.</string>
<string name="settings_public_api_dashboard_enabled_summary_off">Ghi lại dữ liệu sẽ không được miễn phí tự động với các ứng dụng khác.</string>
<string name="settings_public_api_dashboard_enabled_title">Chuyển dữ liệu tự động</string>
<string name="settings_export_filename_title">Định dạng tên tệp</string>
<string name="show_on_dashboard_not_installed">Không có ứng dụng hỗ trợ API OpenTracks Dashboard nào được cài đặt.</string>
<string name="description_pace_nautical">Nhịp độ (phút/dặm hải lý)</string>
<string name="image_discard">Bỏ</string>
<string name="settings_announcements_average_heart_rate">Nhịp tim trung bình</string>
<string name="settings_announcements_lap_heart_rate">Nhịp tim vòng</string>
<string name="settings_announcements_average_speed_pace">Tốc độ trung bình/nhịp độ</string>
<string name="settings_stats_units_nautical">Hải lý (dặm hải lý, ft)</string>
<string name="settings_public_api_package_title">Tên gói cho API công khai</string>
<plurals name="voiceDistanceNauticalMiles">
<item quantity="other">%1$.2f dặm hải lý</item>
</plurals>
<string name="select_show_on_map_behavior">Chọn hành vi \"hiện trên bản đồ\"</string>
<string name="settings_announcements_lap_speed_pace">Tốc độ vòng/nhịp độ</string>
<string name="track_discarding">Đang bỏ đoạn đường</string>
<plurals name="voiceSpeedMKnots">
<item quantity="other">%1$.1f nút</item>
</plurals>
<string name="voice_per_nautical_mile">trên dặm hải lý</string>
<string name="description_speed_nautical">Tốc độ (nút)</string>
<string name="unit_nautical_mile">Dặm hải lý</string>
<string name="unit_knots">nút</string>
<string name="unit_minute_per_nautical_mile">phút/dặm hải lý</string>
<string name="value_integer_knots">%1$d nút</string>
<string name="value_float_knots">%1$.1f nút</string>
<string name="value_float_knots_recommended">%1$.1f nút (khuyên dùng)</string>
<string name="error_crash_title">%1$s đã dừng đột ngột</string>
<string name="share_crash_report">Chia sẻ báo cáo dừng đột ngột</string>
<string name="report_bug">Báo cáo lỗi</string>
<string name="show_on_map_format_ask">Hỏi</string>
<string name="show_on_dashboard">API OpenTracks Dashboard</string>
<string name="always">Luôn luôn</string>
</resources>
+3
View File
@@ -46,6 +46,8 @@
<string name="show_on_map_format_key" translatable="false">showOnMapFormatKey</string>
<string name="total_rows_deleted_key" translatable="false">totalRowsDeletedKey</string>
<string name="settings_default_export_directory_key" translatable="false">settingsDefaultExportDirectory</string>
<string name="post_workout_export_enabled_key" translatable="false">instantExportEnabled</string>
<bool name="post_workout_export_enabled_default" translatable="false">false</bool>
@@ -314,6 +316,7 @@
<bool name="import_prevent_reimport_default">true</bool>
<string name="settings_api_key" translatable="false">settingsAPI</string>
<string name="publicapi_package_key" translatable="false">apiPackageKey</string>
<string name="publicapi_enabled_key" translatable="false">apiEnabledKey</string>
<bool name="publicapi_enabled_default">false</bool>
<string name="publicapi_dashboard_enabled_key" translatable="false">apiDashboardEnabledKey</string>
+2 -1
View File
@@ -442,6 +442,7 @@ limitations under the License.
<!-- Settings public api -->
<string name="settings_api_title">Public API</string>
<string name="settings_public_api_package_title">Package name for Public API</string>
<string name="settings_public_api_enabled_title">Public API</string>
<string name="settings_public_api_enabled_summary_on">Other installed applications can start or stop recordings.</string>
<string name="settings_public_api_enabled_summary_off">Only OpenTracks can start and stop recordings.</string>
@@ -493,7 +494,7 @@ limitations under the License.
<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_progress_message">Deleting&#8230;</string>
<string name="track_delete_progress">%1$d/%2$d</string>
<string name="track_delete_number_of_tracks">%1$d tracks</string>
<string name="track_delete_not_recording">This track cannot be deleted as it is currently recorded.</string>
<string name="track_discarding">Discarding Track</string>
<!-- Track Detail -->

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