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