Compare commits

...

36 Commits

Author SHA1 Message Date
Dennis Guse ca2bc6426c Release: v3.13.2 2020-11-22 20:21:50 +01:00
Dennis Guse 81097aac40 Bugfix: crash in landscape mode in TrackController due to wrong background. 2020-11-22 20:21:11 +01:00
Dennis Guse a74b5fc743 Cleanup. 2020-11-21 18:42:32 +01:00
Dennis Guse e996ad0a0c Release: v3.13.1 2020-11-21 10:37:25 +01:00
Dennis Guse 0f726958e0 Revert "Shrinking all APK builds without obfuscation. Fixes #433."
Introduced #523.

This reverts commit 34834120
2020-11-21 10:34:15 +01:00
Peter Storch 3a84520819 removed duplicate export 2020-11-20 21:56:39 +01:00
Dennis Guse 8328069b9d README: add human tech badge :)
Fixes #521.
2020-11-20 21:51:45 +01:00
Peter Storch b2df79caf2 fix #516 post workout export fails silently (#520)
Change the post workout export to use the ExportService (JobIntentService) and directing the user to the SettingsActivity in case the export failed to check the export directory.
2020-11-20 20:59:52 +01:00
Dennis Guse 6bcb8db40c Release: v3.13.0 2020-11-17 22:21:24 +01:00
Román Martínez 974baaf611 Marker Bugfixes: markers deletion didn't work and track import didn't include marker's photos. Fixes #514.
Marker edition bugfixed: orphan photos are deleted.
2020-11-17 22:02:26 +01:00
Dennis Guse fd77e77bb9 TrackController: readded stop button's pressed state.
Removed in 18546fd3cc
2020-11-17 21:49:21 +01:00
Dennis Guse 1e8beff015 TrackController: refactor button (delayed action) UI and use handler. 2020-11-17 21:43:12 +01:00
Dennis Guse 96bc347374 Activity icon uses now an image instead of a spinner (which was only used as an icon).
Fixed #503.
2020-11-17 21:43:12 +01:00
dependabot[bot] 8f144d4a97 Bump desugar_jdk_libs from 1.1.0 to 1.1.1
Bumps [desugar_jdk_libs](https://github.com/google/desugar_jdk_libs) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/google/desugar_jdk_libs/releases)
- [Changelog](https://github.com/google/desugar_jdk_libs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/desugar_jdk_libs/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-13 07:26:29 +01:00
Dennis Guse 3c3641376d Added power meter infos to READMEs. 2020-11-12 17:12:44 +01:00
Dennis Guse 35f4427aec Merge pull request #444 from OpenTracksApp/cyclingPower
Bluetooth Sensor: add cycling powermeter.
2020-11-12 12:00:13 +01:00
Dennis Guse 5a0cb10838 Bugfix: cadence and speed now can compare when data is not set. 2020-11-11 21:25:56 +01:00
Dennis Guse bf92851c97 Show sensor data's unit. 2020-11-11 21:08:39 +01:00
Dennis Guse 55c45b2050 Bugfix: SensorDataCycling.toString() should use attributes instead of their getters. 2020-11-11 21:08:39 +01:00
Román Martínez d01715444f Adaptive recording layout for sensors (heart rate, cycling cadence and cycling power). 2020-11-11 21:08:39 +01:00
Dennis Guse 634a7a4321 Bluetooth Sensor: add cycling powermeter. 2020-11-11 21:08:39 +01:00
Dennis Guse e5911a79ab Translation: Norwegian used wrong language code.
Introduced in 56aab12110
Fixes #515.
2020-11-11 20:07:59 +01:00
dependabot[bot] a78b3eb66d Bump gradle from 4.1.0 to 4.1.1
Bumps gradle from 4.1.0 to 4.1.1.

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-11 07:53:54 +01:00
Román Martínez 28890abd47 Bugfix: view changed from wrong thread. Fixes #509. 2020-11-09 20:19:08 +01:00
Dennis Guse 0a466d240b Wrap searchQuery in SQLite wildcards. 2020-11-09 18:56:25 +01:00
Dennis Guse 9ef312fc93 Search is now handled by TrackListActivity and MarkerListActivity.
* data is loaded via cursor,
* removed custom ranking (handled in database),
* search text is wrapped with wildcards, and
* search view can be closed by back button.

Fixes #492.
2020-11-09 18:56:25 +01:00
Dennis Guse 56aab12110 Noway's translation was included twice. 2020-11-09 18:55:38 +01:00
Dennis Guse c394448273 Removed Transifex configuration. 2020-11-09 18:41:38 +01:00
Dennis Guse 95671f4927 Translation is now on Weblate. 2020-11-09 18:27:38 +01:00
Dennis Guse 348341204e Shrinking all APK builds without obfuscation.
Fixes #433.
2020-11-09 12:09:37 +01:00
Román Martínez 82f5c127ee Bugfix: NPE in import. Fixes #506. 2020-11-08 14:33:31 +01:00
Dennis Guse dd490f528e Cursors must be closed. 2020-11-08 00:37:01 +01:00
Christoph Wurst 27731319f0 Fix internet access typo in readme
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
2020-11-07 10:05:41 +01:00
Christoph Wurst 86bfe6d0ce Fix formatting of md lists, remove trailing whitespaces
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
2020-11-07 09:21:55 +01:00
Christoph Wurst ea8bbb0b36 Update list of tested sensors with Polar H10 and Garmin SPD/CAD
Adds three tested sensors
* "Polar H10" heart rate sensor
* "Garmin Cadence Sensor 2" cadence sensor
* "Garmin Speed Sensor 2" speed sensor

Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
2020-11-07 09:21:55 +01:00
dependabot[bot] c02fdd3bba Bump desugar_jdk_libs from 1.0.10 to 1.1.0
Bumps [desugar_jdk_libs](https://github.com/google/desugar_jdk_libs) from 1.0.10 to 1.1.0.
- [Release notes](https://github.com/google/desugar_jdk_libs/releases)
- [Changelog](https://github.com/google/desugar_jdk_libs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/desugar_jdk_libs/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-04 07:46:08 +01:00
78 changed files with 1141 additions and 2447 deletions
-10
View File
@@ -1,10 +0,0 @@
[main]
host = https://www.transifex.com
[opentracks.src-main-res-values-strings-xml--main]
file_filter = src/main/res/values-<lang>/strings.xml
minimum_perc = 0
source_file = src/main/res/values/strings.xml
source_lang = en
type = ANDROID
+6 -3
View File
@@ -2,6 +2,8 @@
_OpenTracks_ is a sport tracking application that completely respects your privacy.
[![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech)
<table>
<tr>
<th>Free</th>
@@ -20,8 +22,8 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
</a>
</td>
<td align="center">
<a href="https://www.transifex.com/opentracks/opentracks/">
Transifex
<a href="https://weblate.bubu1.eu/engage/OpenTracks/">
<img src="https://weblate.bubu1.eu/widgets/OpenTracks/-/opentracks/svg-badge.svg" alt="Translation status" />
</a>
</td>
</tr>
@@ -53,6 +55,7 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
* __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__
@@ -68,7 +71,7 @@ __Only required permissions:__
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 required Internet access).
_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.
+1 -1
View File
@@ -34,4 +34,4 @@ Requirement: `gem install fastlane`
## Translations (localization)
Translations of the OpenTracks are handled in Transifex: [https://www.transifex.com/opentracks](https://www.transifex.com/opentracks/).
Translations of the OpenTracks are handled in Weblate: [https://weblate.bubu1.eu/projects/OpenTracks/](https://weblate.bubu1.eu/projects/OpenTracks/).
+23 -2
View File
@@ -1,21 +1,42 @@
# Overview of tested Bluetooth LE sensors
## HRS: Heartrate Service
* Decathlon|Geonaute|Kalenji Dual HRM Belt
* moofit heart rate monitor
* Polar H7
* Polar H10
* Polar OH1
Has updatable firmware that requires a user account at polar.com
* Wahoo Tickr (Model: WFBTHR02)
## CSCP: Cycling Cadence and Speed Service
Please note that the according to the specification these sensors _may_ provide speed as well as cadence.
However, often only one value is provided.
### Speed
* Garmin Speed Sensor 2
* Wahoo Speed (Model: WFRPMSPD)
### Cadence
### Cadence
* Garmin Cadence Sensor 2
* Wahoo Cadence (Model: WFPODCAD2)
This sensor reports cadence data as speed.
A workaround is in place.
A workaround is in place.
## Cycling Power Measurement
We only support _Instantaneous Power_.
_WARNING_: we only support physically movements with valid GPS coordinates.
Using a device indoors (no GPS available) will only show the current power values, but won't store anything.
Also the distance is not computed.
* Assioma Duo Powermeter
* Elite drivo II
* QUARQ Red DZero Powermeter
* Tacx Satori Smart
* Wahoo Kickr v4.0
+4 -4
View File
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.android.tools.build:gradle:4.1.1'
}
}
@@ -51,8 +51,8 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 3704
versionName "v3.12.0"
versionCode 3740
versionName "v3.13.2"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -86,7 +86,7 @@ android {
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference:1.1.1'
+9 -10
View File
@@ -10,15 +10,11 @@ endlegend
[*] --> TrackListActivity
TrackListActivity --> TrackRecordingActivity : Start recording
TrackListActivity --> SearchListActivity
TrackListActivity --> TrackRecordedActivity : Show track
TrackListActivity --> MarkerListActivity : Show all markers
TrackListActivity --> AggregatedStatisticsActivity : Aggregated stats
TrackListActivity --> TrackRecordedActivity : Show track
TrackListActivity --> TrackDeleteActivity
SearchListActivity --> MarkerDetailActivity : Search result
SearchListActivity --> TrackRecordedActivity : Search result
TrackRecordedActivity --> TrackDeleteActivity
TrackRecordedActivity --> TrackEditActivity
TrackRecordedActivity --> MarkerListActivity
@@ -46,11 +42,14 @@ MarkerDetailActivity --> CoordinateOnMapIntent : URI
SettingsActivity --> AboutActivity
SettingsActivity --> HelpActivity
SettingsActivity --> ImportActivity
SettingsActivity --> ExportActivity
SettingsActivity --> DirectoryChooserActivity$ImportDirectoryChooserActivity
SettingsActivity --> DirectoryChooserActivity$ExportDirectoryChooserActivity
SettingsActivity --> DirectoryChooserActivity$DefaultTrackExportDirectoryChooserActivity
DirectoryChooserActivity$ImportDirectoryChooserActivity --> ImportActivity
DirectoryChooserActivity$ExportDirectoryChooserActivity --> ExportActivity
SettingsActivity --> DirectoryChooserActivity.ImportDirectoryChooserActivity
SettingsActivity --> DirectoryChooserActivity.ExportDirectoryChooserActivity
SettingsActivity --> DirectoryChooserActivity.DefaultTrackExportDirectoryChooserActivity
DirectoryChooserActivity.ImportDirectoryChooserActivity --> ImportActivity
DirectoryChooserActivity.ExportDirectoryChooserActivity --> ExportActivity
FileImportIntent --> ImportActivity
@enduml
@@ -5,6 +5,6 @@ Funktionen & Features:
* zeichnet Sport und andere Outdooraktivitäten auf
* unterstützt das Markieren interessanter Orte, inkl. Foto-Funktion
* zeichnet deine Herzfrequenz auf (Bluetooth LE)
* nutze beim Radfahren deine Trittfrequenz- und Geschwindigkeitssensoren (Bluetooth LE)
* nutze beim Radfahren deine Trittfrequenz- und Geschwindigkeitssensoren sowie Leistungsmesser (Bluetooth LE)
* exportiert deine Daten als KML oder GPX
* funktioniert ganz ohne Internet
@@ -0,0 +1,10 @@
v3.13.0: OpenTracks
Changes:
- Sensor: support for powermeter (BLE)
- Search by track and marker (in lists)
- Combined search removed
Bugfix:
- Import crashed in UI
- Images of markers were not imported
@@ -0,0 +1,5 @@
v3.13.1: OpenTracks
Bugfix:
- Crash on record start: minify/shrink removed a drawable
- Automatic export failed silently (mainly remote destinations)
@@ -0,0 +1,5 @@
v3.13.2: OpenTracks
Bugfix:
- Crash in landscape mode
@@ -6,5 +6,6 @@ Features:
* 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
@@ -1,323 +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.content;
import android.content.ContentUris;
import android.location.Location;
import android.net.Uri;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.provider.ProviderTestRule;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.content.provider.CustomSQLiteOpenHelper;
import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
/**
* Tests for {@link SearchEngine}.
* These are not meant to be quality tests, but instead feature-by-feature tests.
* In other words, they don't test the mixing of different score boostings, just each boosting separately.
*
* @author Rodrigo Damazio
*/
public class SearchEngineTest {
private static final String TAG = SearchEngineTest.class.getSimpleName();
private static final Location HERE = new Location("gps");
private static final long NOW = SearchEngine.OLDEST_ALLOWED_TIMESTAMP + 1000;
@Rule
public ProviderTestRule sqliteContentProviderRule = new ProviderTestRule.Builder(CustomContentProvider.class, ContentProviderUtils.AUTHORITY_PACKAGE).setPrefix(TAG).build();
private ContentProviderUtils providerUtils;
private SearchEngine engine;
@Before
public void setUp() {
providerUtils = new ContentProviderUtils(sqliteContentProviderRule.getResolver());
engine = new SearchEngine(providerUtils);
}
@After
public void tearDown() {
InstrumentationRegistry.getInstrumentation().getTargetContext().deleteDatabase(TAG + CustomSQLiteOpenHelper.DATABASE_NAME);
}
private Track.Id insertTrack(String title, String description, String category, long hoursAgo) {
Track track = new Track();
track.setName(title);
track.setDescription(description);
track.setCategory(category);
TrackStatistics stats = track.getTrackStatistics();
if (hoursAgo > 0) {
// Started twice hoursAgo, so the average time is hoursAgo.
stats.setStartTime_ms(NOW - hoursAgo * 1000L * 60L * 60L * 2);
stats.setStopTime_ms(NOW);
}
Uri uri = providerUtils.insertTrack(track);
return new Track.Id(ContentUris.parseId(uri));
}
private Track.Id insertTrack(String title, String description, String category) {
return insertTrack(title, description, category, -1);
}
private Track.Id insertTrack(String title, long hoursAgo) {
return insertTrack(title, "", "", hoursAgo);
}
private Marker.Id insertWaypoint(String title, String description, String category, double distance, long hoursAgo, Track.Id trackId) {
Location location = new Location(HERE);
location.setLatitude(location.getLatitude() + distance);
location.setLongitude(location.getLongitude() + distance);
if (hoursAgo >= 0) {
location.setTime(NOW - hoursAgo * 1000L * 60L * 60L);
}
Marker waypoint = new Marker(trackId, location);
waypoint.setName(title);
waypoint.setDescription(description);
waypoint.setCategory(category);
Uri uri = providerUtils.insertMarker(waypoint);
return new Marker.Id(ContentUris.parseId(uri));
}
private Marker.Id insertWaypoint(String title, String description, String category, Track.Id trackId) {
return insertWaypoint(title, description, category, 0.0, -1, trackId);
}
private Marker.Id insertWaypoint(String title, double distance, Track.Id trackId) {
return insertWaypoint(title, "", "", distance, -1, trackId);
}
private Marker.Id insertWaypoint(String title, long hoursAgo, Track.Id trackId) {
return insertWaypoint(title, "", "", 0.0, hoursAgo, trackId);
}
@Test
public void testSearchText() {
// Insert 7 tracks (purposefully out of result order):
// - one which won't match
// - one which will match the description
// - one which will match the category
// - one which will match the title
// - one which will match in title and category
// - one which will match in title and description
// - one which will match in all fields
insertTrack("bb", "cc", "dd");
Track.Id descriptionMatchId = insertTrack("bb", "aa", "cc");
Track.Id categoryMatchId = insertTrack("bb", "cc", "aa");
Track.Id titleMatchId = insertTrack("aa", "bb", "cc");
Track.Id titleCategoryMatchId = insertTrack("aa", "bb", "ca");
Track.Id titleDescriptionMatchId = insertTrack("aa", "ba", "cc");
Track.Id allMatchId = insertTrack("aa", "ba", "ca");
SearchQuery query = new SearchQuery("a", null, null, NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Title > Description > Category.
assertTrackResults(results, allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId, categoryMatchId);
}
@Test
public void testSearchWaypointText() {
// Insert 7 waypoints (purposefully out of result order):
// - one which won't match
// - one which will match the description
// - one which will match the category
// - one which will match the title
// - one which will match in title and category
// - one which will match in title and description
// - one which will match in all fields
Track.Id trackId = insertTrack("", "", "");
insertWaypoint("bb", "cc", "dd", trackId);
Marker.Id descriptionMatchId = insertWaypoint("bb", "aa", "cc", trackId);
Marker.Id categoryMatchId = insertWaypoint("bb", "cc", "aa", trackId);
Marker.Id titleMatchId = insertWaypoint("aa", "bb", "cc", trackId);
Marker.Id titleCategoryMatchId = insertWaypoint("aa", "bb", "ca", trackId);
Marker.Id titleDescriptionMatchId = insertWaypoint("aa", "ba", "cc", trackId);
Marker.Id allMatchId = insertWaypoint("aa", "ba", "ca", trackId);
SearchQuery query = new SearchQuery("a", null, null, NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Title > Description > Category.
assertWaypointResults(results, allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId, categoryMatchId);
}
@Test
public void testSearchMixedText() {
// Insert 5 entries (purposefully out of result order):
// - one waypoint which will match by description
// - one waypoint which won't match
// - one waypoint which will match by title
// - one track which won't match
// - one track which will match by title
Track.Id trackId = insertTrack("aa", "bb", "cc");
Marker.Id descriptionWaypointId = insertWaypoint("bb", "aa", "cc", trackId);
insertWaypoint("bb", "cc", "dd", trackId);
Marker.Id titleWaypointId = insertWaypoint("aa", "bb", "cc", trackId);
insertTrack("bb", "cc", "dd");
SearchQuery query = new SearchQuery("a", null, null, NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Title > Description > Category.
assertEquals(results.toString(), 3, results.size());
assertTrackResult(trackId, results.get(0));
assertWaypointResult(titleWaypointId, results.get(1));
assertWaypointResult(descriptionWaypointId, results.get(2));
}
@Test
public void testSearchWaypointDistance() {
// All results match text, but they're at difference distances from the user.
Track.Id trackId = insertTrack("", "", "");
Marker.Id farFarAwayId = insertWaypoint("aa", 0.3, trackId);
Marker.Id nearId = insertWaypoint("ab", 0.1, trackId);
Marker.Id farId = insertWaypoint("ac", 0.2, trackId);
SearchQuery query = new SearchQuery("a", HERE, null, NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Distance order.
assertWaypointResults(results, nearId, farId, farFarAwayId);
}
public void testSearchTrackRecent() {
// All results match text, but they're were recorded at different times.
Track.Id oldestId = insertTrack("aa", 3);
Track.Id recentId = insertTrack("ab", 1);
Track.Id oldId = insertTrack("ac", 2);
SearchQuery query = new SearchQuery("a", null, null, NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Reverse time order.
assertTrackResults(results, recentId, oldId, oldestId);
}
@Test
public void testSearchWaypointRecent() {
// All results match text, but they're were recorded at different times.
Track.Id trackId = insertTrack("", "", "");
Marker.Id oldestId = insertWaypoint("aa", 2, trackId);
Marker.Id recentId = insertWaypoint("ab", 0, trackId);
Marker.Id oldId = insertWaypoint("ac", 1, trackId);
SearchQuery query = new SearchQuery("a", null, null, NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Reverse time order.
assertWaypointResults(results, recentId, oldId, oldestId);
}
@Test
public void testSearchCurrentTrack() {
// All results match text, but one of them is the current track.
Track.Id currentId = insertTrack("ab", 1);
Track.Id otherId = insertTrack("aa", 1);
SearchQuery query = new SearchQuery("a", null, currentId, NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Current track should be demoted.
assertTrackResults(results, otherId, currentId);
}
@Test
public void testSearchCurrentTrackWaypoint() {
// All results match text, but one of them is in the current track.
Track.Id trackId1 = insertTrack("", "", "");
Track.Id trackId2 = insertTrack("", "", "");
Marker.Id otherId = insertWaypoint("aa", 1, trackId2);
Marker.Id currentId = insertWaypoint("ab", 1, trackId1);
SearchQuery query = new SearchQuery("a", null, trackId1, NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Waypoint in current track should be promoted.
assertWaypointResults(results, currentId, otherId);
}
private void assertTrackResult(Track.Id trackId, ScoredResult result) {
assertNotNull("Not a track", result.track);
assertNull("Ambiguous result", result.marker);
assertEquals(trackId, result.track.getId());
}
private void assertTrackResults(List<ScoredResult> results, Track.Id... trackIds) {
long[] longTrackIds = new long[trackIds.length];
for (int i = 0; i < trackIds.length; i++) {
longTrackIds[i] = trackIds[i].getId();
}
String errMsg = "Expected IDs=" + Arrays.toString(longTrackIds) + "; results=" + results;
assertEquals(results.size(), trackIds.length);
for (int i = 0; i < results.size(); i++) {
ScoredResult result = results.get(i);
assertNotNull(errMsg, result.track);
assertNull(errMsg, result.marker);
assertEquals(errMsg, trackIds[i], result.track.getId());
}
}
private void assertWaypointResult(Marker.Id waypointId, ScoredResult result) {
assertNotNull("Not a waypoint", result.marker);
assertNull("Ambiguous result", result.track);
assertEquals(waypointId, result.marker.getId());
}
private void assertWaypointResults(List<ScoredResult> results, Marker.Id... waypointIds) {
long[] longWaypointIds = new long[waypointIds.length];
for (int i = 0; i < waypointIds.length; i++) {
longWaypointIds[i] = waypointIds[i].getId();
}
String errMsg = "Expected IDs=" + Arrays.toString(longWaypointIds) + "; results=" + results;
assertEquals(results.size(), waypointIds.length);
for (int i = 0; i < results.size(); i++) {
ScoredResult result = results.get(i);
assertNotNull(errMsg, result.marker);
assertNull(errMsg, result.track);
assertEquals(errMsg, waypointIds[i].getId(), result.marker.getId().getId());
}
}
}
@@ -9,6 +9,7 @@ import de.dennisguse.opentracks.util.UintUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
@RunWith(AndroidJUnit4.class)
public class SensorDataCyclingTest {
@@ -117,4 +118,32 @@ public class SensorDataCyclingTest {
// then
assertEquals(2, current.getSpeed_mps(), 0.01);
}
@Test
public void equals_speed_with_no_data() {
// given
SensorDataCycling.Speed previous = new SensorDataCycling.Speed("sensorAddress");
SensorDataCycling.Speed current = new SensorDataCycling.Speed("sensorAddress", "sensorName", 0, 2048);
// when
previous.toString();
// then
assertNotEquals(previous, current);
assertNotEquals(previous, previous);
}
@Test
public void equals_cadence_with_no_data() {
// given
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress");
SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 0, 2048);
// when
previous.toString();
// then
assertNotEquals(previous, current);
assertNotEquals(previous, previous);
}
}
@@ -9,6 +9,7 @@ import androidx.test.filters.LargeTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -196,6 +197,7 @@ public class ExportImportTest {
assertNull(importedTrack);
}
@Ignore
@LargeTest
@Test
public void kmz_only_track() {
@@ -203,6 +205,7 @@ public class ExportImportTest {
Log.e(TAG, "Test not implemented.");
}
@Ignore
@LargeTest
@Test
public void kmz_with_trackdetail() {
@@ -210,6 +213,7 @@ public class ExportImportTest {
Log.e(TAG, "Test not implemented.");
}
@Ignore
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata() {
@@ -217,6 +221,7 @@ public class ExportImportTest {
Log.e(TAG, "Test not implemented.");
}
@Ignore
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata_and_pictures() {
@@ -75,4 +75,16 @@ public class BluetoothUtilsTest {
assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
}
@Test
public void parseCyclingPower_power() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER_MEASUREMENT_CHAR_UUID, 0, 0);
characteristic.setValue(new byte[]{0, 0, 40, 0});
// when
int power_w = BluetoothUtils.parseCyclingPower(characteristic);
// then
assertEquals(40, power_w);
}
}
+12 -16
View File
@@ -246,16 +246,24 @@ limitations under the License.
<activity
android:name=".MarkerListActivity"
android:label="@string/menu_markers">
android:label="@string/menu_markers"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.default_searchable"
android:value=".SearchListActivity" />
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<activity
android:name=".SearchListActivity"
android:name=".TrackListActivity"
android:theme="@style/SplashTheme"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
@@ -269,18 +277,6 @@ limitations under the License.
<activity android:name=".TrackEditActivity" />
<activity
android:name=".TrackListActivity"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.default_searchable"
android:value=".SearchListActivity" />
</activity>
<activity android:name=".AggregatedStatisticsActivity" />
<activity android:name=".io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity" />
@@ -30,8 +30,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* An abstract class for the following common tasks across
* {@link TrackListActivity}, {@link TrackRecordedActivity}, and
* {@link SearchListActivity}:
* {@link TrackListActivity}, {@link TrackRecordedActivity}, and {@link TrackRecordedActivity}:
* <p>
* - share track <br>
* - delete tracks <br>
@@ -48,7 +47,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DELETE_REQUEST_CODE) {
onDeleted();
onTrackDeleted();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
@@ -93,5 +92,5 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
/**
* Called after {@link TrackDeleteActivity} returns its result.
*/
abstract protected void onDeleted();
abstract protected void onTrackDeleted();
}
@@ -113,7 +113,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
}
@Override
public void onDeleteMarkerDone() {
public void onMarkerDeleted() {
runOnUiThread(this::finish);
}
@@ -37,6 +37,8 @@ import androidx.annotation.NonNull;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
@@ -70,8 +72,12 @@ public class MarkerEditActivity extends AbstractActivity {
private MenuItem insertGalleryImgMenuItem;
private Uri photoUri;
private Uri photoUriOriginal;
private List<Uri> photoUriDeleteList = new ArrayList<>();
private boolean hasCamera;
private boolean isNewMarker;
// UI elements
private MarkerEditBinding viewBinding;
@@ -92,13 +98,41 @@ public class MarkerEditActivity extends AbstractActivity {
marker.setPhotoUrl(null);
}
viewBinding.markerEditPhoto.setImageBitmap(null);
photoUriDeleteList.add(photoUri);
photoUriOriginal = photoUriOriginal == null ? photoUri : photoUriOriginal;
photoUri = null;
hideAndShowOptions();
});
viewBinding.markerEditCancel.setOnClickListener(v -> finish());
viewBinding.markerEditCancel.setOnClickListener(v -> {
Track.Id trackId = getTrackId();
if (isNewMarker && trackId != null) {
// new marker and user cancel -> delete all photos from track directory
for (Uri photoUriDelete : photoUriDeleteList) {
File photoFile = FileUtils.getPhotoFileIfExists(this, trackId, photoUriDelete);
FileUtils.deleteDirectoryRecurse(photoFile);
}
if (photoUri != null) {
File photoFile = FileUtils.getPhotoFileIfExists(this, trackId, photoUri);
FileUtils.deleteDirectoryRecurse(photoFile);
}
} else if (!isNewMarker && trackId != null) {
// no new marker, user cancel and photo was changed -> delete all photos but original one
for (Uri photoUri : photoUriDeleteList) {
if (photoUriOriginal != photoUri) {
File photoFile = FileUtils.getPhotoFileIfExists(this, trackId, photoUri);
FileUtils.deleteDirectoryRecurse(photoFile);
}
}
if (photoUri != null && photoUri != photoUriOriginal) {
File photoFile = FileUtils.getPhotoFileIfExists(this, trackId, photoUri);
FileUtils.deleteDirectoryRecurse(photoFile);
}
}
finish();
});
final boolean isNewMarker = markerId == null;
isNewMarker = markerId == null;
setTitle(isNewMarker ? R.string.menu_insert_marker : R.string.menu_edit);
viewBinding.markerEditDone.setText(isNewMarker ? R.string.generic_add : R.string.generic_save);
@@ -108,6 +142,13 @@ public class MarkerEditActivity extends AbstractActivity {
} else {
saveMarker();
}
Track.Id trackId = getTrackId();
if (trackId == null) {
for (Uri photoUri : photoUriDeleteList) {
File photoFile = FileUtils.getPhotoFileIfExists(this, trackId, photoUri);
FileUtils.deleteDirectoryRecurse(photoFile);
}
}
finish();
});
@@ -16,6 +16,7 @@
package de.dennisguse.opentracks;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -28,9 +29,10 @@ import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
import androidx.cursoradapter.widget.ResourceCursorAdapter;
import androidx.loader.app.LoaderManager;
import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import de.dennisguse.opentracks.content.data.Marker;
@@ -67,7 +69,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
// Note that the key can be null
if (PreferencesUtils.isKey(MarkerListActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(MarkerListActivity.this);
}
@@ -84,6 +85,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
private MarkerListBinding viewBinding;
private final MarkerLoaderCallback loaderCallbacks = new MarkerLoaderCallback();
// Callback when an item is selected in the contextual action mode
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
@@ -135,8 +138,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
int categoryIndex = cursor.getColumnIndex(MarkerColumns.CATEGORY);
int descriptionIndex = cursor.getColumnIndex(MarkerColumns.DESCRIPTION);
int photoUrlIndex = cursor.getColumnIndex(MarkerColumns.PHOTOURL);
int latitudeIndex = cursor.getColumnIndex(MarkerColumns.LATITUDE);
int longitudeIndex = cursor.getColumnIndex(MarkerColumns.LONGITUDE);
int iconId = MarkerUtils.ICON_ID;
String name = cursor.getString(nameIndex);
@@ -144,9 +145,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
String category = cursor.getString(categoryIndex);
String description = cursor.getString(descriptionIndex);
String photoUrl = cursor.getString(photoUrlIndex);
//TODO also show latitude and longitude in list
double latitude = cursor.getDouble(latitudeIndex);
double longitude = cursor.getDouble(longitudeIndex);
ListItemUtils.setListItem(MarkerListActivity.this, view, false, true, iconId, R.string.image_marker, name, null, null, 0, time, false, category, description, photoUrl);
}
@@ -154,23 +152,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
viewBinding.markerList.setAdapter(resourceCursorAdapter);
ActivityUtils.configureListViewContextualMenu(viewBinding.markerList, contextualActionModeCallback);
LoaderManager.getInstance(this).initLoader(0, null, new LoaderCallbacks<Cursor>() {
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return ContentProviderUtils.getMarkersLoader(MarkerListActivity.this, track != null ? track.getId() : null);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
resourceCursorAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
resourceCursorAdapter.swapCursor(null);
}
});
loadData(getIntent());
}
@Override
@@ -230,14 +212,14 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
/**
* Handles a context item selection.
*
* @param itemId the menu item id
* @param longMarkerds the marker ids
* @param itemId the menu item id
* @param longMarkerIds the marker ids
* @return true if handled.
*/
private boolean handleContextItem(int itemId, long... longMarkerds) {
Marker.Id[] markerIds = new Marker.Id[longMarkerds.length];
for (int i = 0; i < longMarkerds.length; i++) {
markerIds[i] = new Marker.Id(longMarkerds[i]);
private boolean handleContextItem(int itemId, long... longMarkerIds) {
Marker.Id[] markerIds = new Marker.Id[longMarkerIds.length];
for (int i = 0; i < longMarkerIds.length; i++) {
markerIds[i] = new Marker.Id(longMarkerIds[i]);
}
Intent intent;
@@ -255,9 +237,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
}
return true;
case R.id.list_context_menu_delete:
if (markerIds.length > 1 && markerIds.length == viewBinding.markerList.getCount()) {
markerIds = null;
}
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), markerIds);
return true;
case R.id.list_context_menu_select_all:
@@ -279,7 +258,92 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
}
@Override
public void onDeleteMarkerDone() {
public void onBackPressed() {
if (loaderCallbacks.getSearchQuery() != null) {
loaderCallbacks.setSearch(null);
return;
}
SearchView searchView = (SearchView) searchMenuItem.getActionView();
if (!searchView.isIconified()) {
searchView.setIconified(true);
return;
}
super.onBackPressed();
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
loadData(intent);
}
private void loadData(Intent intent) {
String searchQuery = null;
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
searchQuery = intent.getStringExtra(SearchManager.QUERY);
}
loaderCallbacks.setSearch(searchQuery);
}
@Override
public void onMarkerDeleted() {
// Do nothing
}
private class MarkerLoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> {
private String searchQuery = null;
public String getSearchQuery() {
return searchQuery;
}
public void setSearch(String searchQuery) {
this.searchQuery = searchQuery;
restart();
if (searchQuery != null) {
setTitle(searchQuery);
} else {
setTitle(R.string.menu_markers);
}
}
public void restart() {
LoaderManager.getInstance(MarkerListActivity.this).restartLoader(0, null, loaderCallbacks);
}
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
final String[] PROJECTION = new String[]{MarkerColumns._ID,
MarkerColumns.NAME, MarkerColumns.DESCRIPTION, MarkerColumns.CATEGORY,
MarkerColumns.TIME, MarkerColumns.PHOTOURL};
if (searchQuery == null) {
if (track != null) {
return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, MarkerColumns.TRACKID + "=?", new String[]{String.valueOf(track.getId().getId())}, null);
} else {
return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, null, null, null);
}
} else {
final String SEARCH_QUERY = MarkerColumns.NAME + " LIKE ? OR " +
MarkerColumns.DESCRIPTION + " LIKE ? OR " +
MarkerColumns.CATEGORY + " LIKE ?";
final String[] selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"};
return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, SEARCH_QUERY, selectionArgs, MarkerColumns.DEFAULT_SORT_ORDER + " DESC");
}
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
resourceCursorAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
resourceCursorAdapter.swapCursor(null);
}
}
}
@@ -1,446 +0,0 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks;
import android.app.SearchManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import de.dennisguse.opentracks.content.SearchEngine;
import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.SearchListBinding;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
/**
* An activity to display a list of searchable results.
*
* @author Rodrigo Damazio
* <p>
* TODO: allow to refine searchable (present searchable in context menu)
*/
public class SearchListActivity extends AbstractListActivity implements DeleteMarkerCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
private static final String TAG = SearchListActivity.class.getSimpleName();
private static final String IS_RECORDING_FIELD = "isRecording";
private static final String IS_PAUSED_FIELD = "isPaused";
private static final String ICON_ID_FIELD = "icon";
private static final String ICON_CONTENT_DESCRIPTION_ID_FIELD = "iconContentDescription";
private static final String NAME_FIELD = "name";
private static final String TOTAL_TIME_FIELD = "totalTime";
private static final String TOTAL_DISTANCE_FIELD = "totalDistance";
private static final String MARKER_COUNT_FIELD = "markerCount";
private static final String START_TIME_FIELD = "startTime";
private static final String CATEGORY_FIELD = "category";
private static final String DESCRIPTION_FIELD = "description";
private static final String PHOTO_URL_FIELD = "photoUrl";
private static final String MARKER_LATITUDE_FIELD = "latitude";
private static final String MARKER_LONGITUDE_FIELD = "longitude";
private static final String TRACK_ID_FIELD = "trackId";
private static final String MARKER_ID_FIELD = "markerId";
private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private SearchEngine searchEngine;
private ArrayAdapter<Map<String, Object>> arrayAdapter;
private boolean metricUnits = true;
private Track.Id recordingTrackId;
private boolean recordingTrackPaused;
private SearchListBinding viewBinding;
// Callback when an item is selected in the contextual action mode
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
boolean isSingleSelection = positions.length == 1;
boolean isSingleSelectionTrack;
if (isSingleSelection) {
Map<String, Object> item = arrayAdapter.getItem(positions[0]);
isSingleSelectionTrack = item.get(MARKER_ID_FIELD) == null;
} else {
isSingleSelectionTrack = false;
}
// Not recording, one item, item is a track
MenuItem shareMenuItem = menu.findItem(R.id.list_context_menu_share);
if (isSingleSelectionTrack) {
shareMenuItem.setVisible(!isRecording);
}
// One item, item is a marker
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection && !isSingleSelectionTrack);
// One item, can be a track or a marker
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
// One item. If track, no restriction.
menu.findItem(R.id.list_context_menu_delete).setVisible(isSingleSelection && isSingleSelectionTrack);
// Disable select all, no action is available for multiple selection
menu.findItem(R.id.list_context_menu_select_all).setVisible(false);
}
@Override
public boolean onClick(int itemId, int[] positions, long[] ids) {
return handleContextItem(itemId, positions);
}
};
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (PreferencesUtils.isKey(SearchListActivity.this, R.string.stats_units_key, key)) {
metricUnits = PreferencesUtils.isMetricUnits(SearchListActivity.this);
}
if (PreferencesUtils.isKey(SearchListActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(SearchListActivity.this);
}
if (PreferencesUtils.isKey(SearchListActivity.this, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(SearchListActivity.this);
}
if (key != null) {
runOnUiThread(() -> arrayAdapter.notifyDataSetChanged());
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection();
searchEngine = new SearchEngine(contentProviderUtils);
arrayAdapter = new ArrayAdapter<Map<String, Object>>(this, R.layout.list_item, R.id.list_item_name) {
@NonNull
@Override
public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) {
View view = convertView;
if (convertView == null) {
view = getLayoutInflater().inflate(R.layout.list_item, parent, false);
}
Map<String, Object> resultMap = getItem(position);
boolean isRecording = (boolean) resultMap.get(IS_RECORDING_FIELD);
boolean isPaused = (boolean) resultMap.get(IS_PAUSED_FIELD);
int iconId = (int) resultMap.get(ICON_ID_FIELD);
int iconContentDescriptionId = (int) resultMap.get(ICON_CONTENT_DESCRIPTION_ID_FIELD);
String name = (String) resultMap.get(NAME_FIELD);
String totalTime = (String) resultMap.get(TOTAL_TIME_FIELD);
String totalDistance = (String) resultMap.get(TOTAL_DISTANCE_FIELD);
int markerCount = (int) resultMap.get(MARKER_COUNT_FIELD);
long startTime = (long) resultMap.get(START_TIME_FIELD);
String category = (String) resultMap.get(CATEGORY_FIELD);
String description = (String) resultMap.get(DESCRIPTION_FIELD);
String photoUrl = (String) resultMap.get(PHOTO_URL_FIELD);
ListItemUtils.setListItem(SearchListActivity.this, view, isRecording, isPaused, iconId,
iconContentDescriptionId, name, totalTime, totalDistance, markerCount,
startTime, false, category, description, photoUrl);
return view;
}
};
// UI elements
viewBinding.searchList.setEmptyView(viewBinding.searchListEmpty);
viewBinding.searchList.setAdapter(arrayAdapter);
viewBinding.searchList.setOnItemClickListener((parent, view, position, id) -> {
Map<String, Object> item = arrayAdapter.getItem(position);
Track.Id trackId = (Track.Id) item.get(TRACK_ID_FIELD);
Marker.Id markerId = (Marker.Id) item.get(MARKER_ID_FIELD);
Intent intent = IntentUtils.newIntent(SearchListActivity.this, TrackRecordedActivity.class);
if (markerId != null) {
intent = intent.putExtra(TrackRecordedActivity.EXTRA_MARKER_ID, markerId);
} else {
intent = intent.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(intent);
});
ActivityUtils.configureListViewContextualMenu(viewBinding.searchList, contextualActionModeCallback);
handleIntent(getIntent());
}
@Override
protected void onStart() {
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
trackRecordingServiceConnection.startConnection(this);
}
@Override
protected void onResume() {
super.onResume();
arrayAdapter.notifyDataSetChanged();
}
@Override
protected void onStop() {
super.onStop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind(this);
}
@Override
protected View getRootView() {
viewBinding = SearchListBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
handleIntent(intent);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH) {
return true;
}
return super.onKeyUp(keyCode, event);
}
/**
* Handles a context item selection.
*
* @param itemId the menu item id
* @param positions the positions of the selected rows
* @return true if handled.
*/
private boolean handleContextItem(int itemId, int[] positions) {
if (positions.length != 1) {
return false;
}
Map<String, Object> item = arrayAdapter.getItem(positions[0]);
Track.Id trackId = (Track.Id) item.get(TRACK_ID_FIELD);
Marker.Id markerId = (Marker.Id) item.get(MARKER_ID_FIELD);
Intent intent;
switch (itemId) {
case R.id.list_context_menu_show_on_map:
IntentUtils.showCoordinateOnMap(this, (double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + "");
return true;
case R.id.list_context_menu_share:
intent = IntentUtils.newShareFileIntent(this, trackId);
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.list_context_menu_edit:
if (markerId != null) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
} else {
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(intent);
// Close the searchable result since its content can change after edit.
finish();
return true;
case R.id.list_context_menu_delete:
if (markerId != null) {
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), markerId);
} else {
deleteTracks(trackId);
}
return true;
default:
return false;
}
}
/**
* Handles the intent.
*
* @param intent the intent
*/
private void handleIntent(Intent intent) {
if (!Intent.ACTION_SEARCH.equals(intent.getAction())) {
Log.e(TAG, "Invalid intent action: " + intent);
finish();
return;
}
final String textQuery = intent.getStringExtra(SearchManager.QUERY);
setTitle(textQuery);
doSearch(textQuery);
}
/**
* Do the searchable.
*
* @param textQuery the query
*/
private void doSearch(String textQuery) {
SearchQuery query = new SearchQuery(textQuery, null, null, System.currentTimeMillis());
SortedSet<ScoredResult> scoredResults = searchEngine.search(query);
final List<Map<String, Object>> displayResults = prepareResultsforDisplay(scoredResults);
// Use the UI thread to display the results
runOnUiThread(() -> {
arrayAdapter.clear();
arrayAdapter.addAll(displayResults);
});
}
/**
* Prepares the result for display.
*
* @param scoredResults a list of score results
* @return a list of result maps
*/
private List<Map<String, Object>> prepareResultsforDisplay(Collection<ScoredResult> scoredResults) {
//TODO Replace use of map<string, object>, but rather provide Track or Marker directly.
ArrayList<Map<String, Object>> output = new ArrayList<>(scoredResults.size());
for (ScoredResult result : scoredResults) {
if (result.track != null) {
output.add(prepareTrackForDisplay(result.track));
} else {
output.add(prepareMarkerForDisplay(result.marker));
}
}
return output;
}
private Map<String, Object> prepareMarkerForDisplay(Marker marker) {
Map<String, Object> resultMap = new HashMap<>();
//TODO: It may be more appropriate to obtain the track name as a join in the retrieval phase of the searchable.
String trackName = null;
Track.Id trackId = marker.getTrackId();
if (!trackId.isValid()) {
Track track = contentProviderUtils.getTrack(trackId);
if (track != null) {
trackName = track.getName();
}
}
resultMap.put(IS_RECORDING_FIELD, false);
resultMap.put(IS_PAUSED_FIELD, true);
resultMap.put(ICON_ID_FIELD, MarkerUtils.ICON_ID);
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_marker);
resultMap.put(NAME_FIELD, marker.getName());
// Display the marker's track name in the total time field
resultMap.put(TOTAL_TIME_FIELD, trackName == null ? null : getString(R.string.search_list_marker_track_location, trackName));
resultMap.put(TOTAL_DISTANCE_FIELD, null);
resultMap.put(MARKER_COUNT_FIELD, 0);
resultMap.put(START_TIME_FIELD, marker.getLocation().getTime());
resultMap.put(CATEGORY_FIELD, marker.getCategory());
resultMap.put(DESCRIPTION_FIELD, marker.getDescription());
resultMap.put(PHOTO_URL_FIELD, marker.getPhotoUrl());
resultMap.put(TRACK_ID_FIELD, marker.getTrackId());
resultMap.put(MARKER_ID_FIELD, marker.getId());
resultMap.put(MARKER_LATITUDE_FIELD, marker.getLocation().getLatitude());
resultMap.put(MARKER_LONGITUDE_FIELD, marker.getLocation().getLongitude());
return resultMap;
}
/**
* Prepares a track for display by filling in a result map.
*
* @param track the track
* @return the result map
*/
private Map<String, Object> prepareTrackForDisplay(Track track) {
Map<String, Object> resultMap = new HashMap<>();
TrackStatistics trackStatistics = track.getTrackStatistics();
String icon = track.getIcon();
String category = icon != null && !icon.equals("") ? null : track.getCategory();
resultMap.put(IS_RECORDING_FIELD, track.getId().equals(recordingTrackId));
resultMap.put(IS_PAUSED_FIELD, recordingTrackPaused);
resultMap.put(ICON_ID_FIELD, TrackIconUtils.getIconDrawable(icon));
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_track);
resultMap.put(NAME_FIELD, track.getName());
resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(trackStatistics.getTotalTime()));
resultMap.put(TOTAL_DISTANCE_FIELD, StringUtils.formatDistance(this, trackStatistics.getTotalDistance(), metricUnits));
resultMap.put(MARKER_COUNT_FIELD, contentProviderUtils.getMarkerCount(track.getId()));
resultMap.put(START_TIME_FIELD, trackStatistics.getStartTime_ms());
resultMap.put(CATEGORY_FIELD, category);
resultMap.put(DESCRIPTION_FIELD, track.getDescription());
resultMap.put(PHOTO_URL_FIELD, null);
resultMap.put(TRACK_ID_FIELD, track.getId());
resultMap.put(MARKER_ID_FIELD, null);
return resultMap;
}
@Override
public void onDeleteMarkerDone() {
runOnUiThread(() -> handleIntent(getIntent()));
}
@Override
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return trackRecordingServiceConnection;
}
@Override
protected void onDeleted() {
runOnUiThread(() -> handleIntent(getIntent()));
}
}
@@ -18,17 +18,12 @@ package de.dennisguse.opentracks;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.os.Handler;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.accessibility.AccessibilityEvent;
import android.widget.ImageButton;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import de.dennisguse.opentracks.databinding.TrackControllerBinding;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
@@ -42,26 +37,29 @@ import de.dennisguse.opentracks.util.UnitConversions;
*
* @author Jimmy Shih
*/
public class TrackController {
public class TrackController implements View.OnTouchListener {
private static final String TAG = TrackController.class.getSimpleName();
private final Activity activity;
private final TrackRecordingServiceConnection trackRecordingServiceConnection;
private final Handler handlerUpdateTotalTime;
private final Handler handlerUpdateTotalTime = new Handler();
private final boolean alwaysShow;
private ButtonDelay buttonDelay;
private Runnable buttonDelay;
private final int buttonDelayDuration;
private final TrackControllerBinding viewBinding;
private final TransitionDrawable transition;
private boolean isRecording;
private boolean isPaused;
private long totalTime = 0;
private long totalTimeTimestamp = 0;
private boolean isResumed = false;
private final Callback callback;
// A runnable to update the total time.
private final Runnable updateTotalTimeRunnable = new Runnable() {
public void run() {
@@ -73,98 +71,98 @@ public class TrackController {
};
@SuppressLint("ClickableViewAccessibility")
TrackController(Activity activity, TrackControllerBinding viewBinding, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, OnClickListener recordListener, OnClickListener stopListener) {
TrackController(Activity activity, TrackControllerBinding viewBinding, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, Callback callback) {
this.activity = activity;
this.viewBinding = viewBinding;
this.trackRecordingServiceConnection = trackRecordingServiceConnection;
this.alwaysShow = alwaysShow;
handlerUpdateTotalTime = new Handler();
viewBinding.trackControllerRecord.setOnTouchListener((view, motionEvent) -> onRecordTouch(activity, recordListener, motionEvent));
this.callback = callback;
viewBinding.trackControllerStop.setOnTouchListener((view, motionEvent) -> onStopTouch(activity, stopListener, motionEvent));
buttonDelayDuration = activity.getResources().getInteger(R.integer.buttonDelayMillis);
viewBinding.trackControllerRecord.setOnTouchListener(this);
viewBinding.trackControllerRecord.setOnClickListener((view) -> {
if (buttonDelay != null || (isRecording && !isPaused)) {
return;
}
callback.recordStart();
});
viewBinding.trackControllerStop.setOnTouchListener(this);
transition = (TransitionDrawable) viewBinding.trackControllerContainer.getBackground();
}
private boolean onRecordTouch(final Activity activity, final OnClickListener recordListener, final MotionEvent motionEvent) {
if (isRecording && !isPaused) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
buttonDelay = new ButtonDelay(activity, viewBinding.trackControllerRecord, R.drawable.ic_button_pause_anim, R.string.hold_to_pause, recordListener);
new Thread(buttonDelay).start();
return true;
} else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
viewBinding.trackControllerRecord.setImageResource(R.drawable.ic_button_pause);
if (buttonDelay != null) {
buttonDelay.canceled = true;
}
return true;
}
} else if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
recordListener.onClick(null);
@SuppressLint("ClickableViewAccessibility") //We do accessibility manually.
@Override
public boolean onTouch(final View view, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
return false;
}
if (buttonDelay != null && event.getAction() == MotionEvent.ACTION_UP) {
handlerUpdateTotalTime.removeCallbacks(buttonDelay);
buttonDelay = null;
((ImageButton) view).setPressed(false);
transition.resetTransition();
showStatusSetDefaultText();
return false;
}
//Trigger button pressed animation
((ImageButton) view).setPressed(true);
//To pause a recording
if (viewBinding.trackControllerRecord.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& isRecording && !isPaused) {
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.recordStart(); //TODO Should be 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
&& 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;
}
private boolean onStopTouch(final Activity activity, final OnClickListener stopListener, final MotionEvent motionEvent) {
if (isRecording) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
buttonDelay = new ButtonDelay(activity, viewBinding.trackControllerStop, R.drawable.ic_button_stop_anim, R.string.hold_to_stop, stopListener);
new Thread(buttonDelay).start();
return true;
} else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
viewBinding.trackControllerStop.setImageResource(R.drawable.ic_button_stop);
if (buttonDelay != null) {
buttonDelay.canceled = true;
}
return true;
}
}
return false;
}
private static class ButtonDelay implements Runnable {
private boolean canceled = false;
private final ImageButton imageButton;
private final Activity activity;
private final OnClickListener clickListener;
private final int delayMillis;
private final Drawable drawable;
private final int delayMessageId;
private ButtonDelay(final Activity activity, final ImageButton imageButton, final int animDrawableId, final int delayMessageId, final OnClickListener clickListener) {
this.activity = activity;
this.clickListener = clickListener;
this.delayMillis = activity.getResources().getInteger(R.integer.buttonDelayMillis);
this.imageButton = imageButton;
this.drawable = ContextCompat.getDrawable(activity, animDrawableId);
this.delayMessageId = delayMessageId;
}
@Override
public void run() {
activity.runOnUiThread(() -> {
imageButton.setImageDrawable(drawable);
if (drawable instanceof AnimatedVectorDrawable) {
((AnimatedVectorDrawable) drawable).start();
}
ActivityUtils.vibrate(activity, 150);
ActivityUtils.toast(activity, delayMessageId, Toast.LENGTH_SHORT, Gravity.TOP);
});
try {
Thread.sleep(delayMillis);
} catch (InterruptedException ignored) {
}
if (!canceled) {
activity.runOnUiThread(() -> {
clickListener.onClick(null);
ActivityUtils.vibrate(activity, 1000);
});
}
}
}
public void update(boolean recording, boolean paused) {
if (!isResumed) {
return;
@@ -182,13 +180,12 @@ public class TrackController {
viewBinding.trackControllerRecord.setImageResource(isRecording && !isPaused ? R.drawable.ic_button_pause : R.drawable.button_record);
viewBinding.trackControllerRecord.setContentDescription(activity.getString(isRecording && !isPaused ? R.string.image_pause : R.string.image_record));
viewBinding.trackControllerStop.setImageResource(isRecording ? R.drawable.ic_button_stop : R.drawable.ic_button_stop_disabled);
viewBinding.trackControllerStop.setEnabled(isRecording);
viewBinding.trackControllerStatus.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
viewBinding.trackControllerStatus.setTextColor(activity.getResources().getColor(isPaused ? android.R.color.white : R.color.recording_text));
viewBinding.trackControllerStatus.setText(isPaused ? R.string.generic_paused : R.string.generic_recording);
showStatusSetDefaultText();
}
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
@@ -228,4 +225,14 @@ public class TrackController {
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
return trackRecordingService != null ? trackRecordingService.getTotalTime() : 0L;
}
private void showStatusSetDefaultText() {
viewBinding.trackControllerStatus.setText(isPaused ? R.string.generic_paused : R.string.generic_recording);
}
public interface Callback {
void recordStart();
void recordStop();
}
}
@@ -18,8 +18,6 @@ package de.dennisguse.opentracks;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ArrayAdapter;
@@ -96,19 +94,8 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
iconValue = track.getIcon();
}
viewBinding.trackEditActivityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(this, iconValue));
viewBinding.trackEditActivityTypeIcon.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.trackEditActivityType.getText().toString());
}
return true;
});
viewBinding.trackEditActivityTypeIcon.setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.trackEditActivityType.getText().toString());
}
return true;
});
setActivityTypeIcon(iconValue);
viewBinding.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.trackEditActivityType.getText().toString()));
viewBinding.trackEditDescription.setText(track.getDescription());
@@ -156,7 +143,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
private void setActivityTypeIcon(String iconValue) {
this.iconValue = iconValue;
TrackIconUtils.setIconSpinner(viewBinding.trackEditActivityTypeIcon, iconValue);
viewBinding.trackEditActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
}
@Override
@@ -33,7 +33,6 @@ import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -41,7 +40,6 @@ import androidx.appcompat.widget.SearchView;
import androidx.core.app.ActivityCompat;
import androidx.cursoradapter.widget.ResourceCursorAdapter;
import androidx.loader.app.LoaderManager;
import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
@@ -60,14 +58,13 @@ import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils;
/**
* An activity displaying a list of tracks.
*
* @author Leif Hendrik Wilden
*/
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackController.Callback {
private static final String TAG = TrackListActivity.class.getSimpleName();
@@ -80,26 +77,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
private TrackListBinding viewBinding;
private final LoaderCallbacks<Cursor> loaderCallbacks = new LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME,
TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME,
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON, "markerCount"};
return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, null, null, TrackUtils.TRACK_SORT_ORDER);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
resourceCursorAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
resourceCursorAdapter.swapCursor(null);
}
};
private final TrackLoaderCallBack loaderCallbacks = new TrackLoaderCallBack();
// Preferences
private boolean metricUnits = true;
@@ -117,8 +95,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
@Override
public boolean onClick(int itemId, int[] positions, long[] ids) {
return handleContextItem(itemId, ids);
public boolean onClick(int itemId, int[] positions, long[] trackIds) {
return handleContextItem(itemId, trackIds);
}
};
@@ -142,7 +120,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
if (key != null) {
runOnUiThread(() -> {
TrackListActivity.this.invalidateOptionsMenu();
LoaderManager.getInstance(TrackListActivity.this).restartLoader(0, null, loaderCallbacks);
loaderCallbacks.restart();
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
trackController.update(isRecording, recordingTrackPaused);
});
@@ -154,14 +132,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem;
private final OnClickListener stopListener = new OnClickListener() {
@Override
public void onClick(View v) {
updateMenuItems(false, false);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this, true);
}
};
// Callback when the trackRecordingServiceConnection binding changes.
private final Runnable bindChangedCallback = new Runnable() {
@Override
@@ -196,33 +166,13 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
};
private final OnClickListener recordListener = new OnClickListener() {
public void onClick(View v) {
if (!PreferencesUtils.isRecording(recordingTrackId)) {
// Not recording -> Recording
updateMenuItems(false, true);
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
startActivity(newIntent);
} else if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false, true);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(false, true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
// Reset theme after splash
setTheme(R.style.ThemeCustom);
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
gpsStatusValue = GpsStatusValue.GPS_NONE;
@@ -231,9 +181,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, true, recordListener, stopListener);
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, true, this);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
// Show trackController when search dialog is dismissed
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
@@ -287,10 +236,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
};
viewBinding.trackList.setAdapter(resourceCursorAdapter);
ActivityUtils.configureListViewContextualMenu(viewBinding.trackList, contextualActionModeCallback);
LoaderManager.getInstance(this).initLoader(0, null, loaderCallbacks);
loadData(getIntent());
requestGPSPermissions();
}
@@ -304,6 +252,14 @@ 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();
@@ -314,14 +270,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackController.onResume(PreferencesUtils.isRecording(recordingTrackId), recordingTrackPaused);
}
@Override
protected void onPause() {
super.onPause();
// Update UI
trackController.onPause();
}
@Override
protected void onStop() {
super.onStop();
@@ -420,6 +368,36 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
return super.onKeyUp(keyCode, event);
}
@Override
public void onBackPressed() {
if (loaderCallbacks.getSearchQuery() != null) {
loaderCallbacks.setSearch(null);
return;
}
SearchView searchView = (SearchView) searchMenuItem.getActionView();
if (!searchView.isIconified()) {
searchView.setIconified(true);
return;
}
super.onBackPressed();
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
loadData(intent);
}
private void loadData(Intent intent) {
String searchQuery = null;
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
searchQuery = intent.getStringExtra(SearchManager.QUERY);
}
loaderCallbacks.setSearch(searchQuery);
}
@Override
public boolean onSearchRequested() {
// Hide trackController when search dialog is shown
@@ -433,7 +411,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
@Override
protected void onDeleted() {
protected void onTrackDeleted() {
// Do nothing
}
@@ -516,4 +494,83 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
return false;
}
private class TrackLoaderCallBack implements LoaderManager.LoaderCallbacks<Cursor> {
private String searchQuery = null;
public String getSearchQuery() {
return searchQuery;
}
public void setSearch(String searchQuery) {
this.searchQuery = searchQuery;
restart();
if (searchQuery != null) {
setTitle(searchQuery);
} else {
setTitle(R.string.app_name);
}
}
public void restart() {
LoaderManager.getInstance(TrackListActivity.this).restartLoader(0, null, loaderCallbacks);
}
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME,
TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME,
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON, TracksColumns.MARKER_COUNT};
final String sortOrder = TracksColumns.STARTTIME + " DESC";
if (searchQuery == null) {
return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, null, null, sortOrder);
} else {
final String SEARCH_QUERY = TracksColumns.NAME + " LIKE ? OR " +
TracksColumns.DESCRIPTION + " LIKE ? OR " +
TracksColumns.CATEGORY + " LIKE ?";
final String[] selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"};
return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, SEARCH_QUERY, selectionArgs, sortOrder);
}
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
resourceCursorAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
resourceCursorAdapter.swapCursor(null);
}
}
@Override
public void recordStart() {
if (!PreferencesUtils.isRecording(recordingTrackId)) {
// Not recording -> Recording
updateMenuItems(false, true);
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
startActivity(newIntent);
} else if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false, true);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(false, true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
}
@Override
public void recordStop() {
updateMenuItems(false, false);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this, true);
}
}
@@ -188,7 +188,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
}
@Override
protected void onDeleted() {
protected void onTrackDeleted() {
runOnUiThread(this::finish);
}
@@ -9,7 +9,6 @@ import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import androidx.annotation.NonNull;
@@ -42,7 +41,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 {
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, TrackController.Callback {
public static final String EXTRA_TRACK_ID = "track_id";
@@ -126,36 +125,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private MenuItem insertMarkerMenuItem;
private MenuItem markerListMenuItem;
private final OnClickListener recordListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
}
};
private final OnClickListener stopListener = new OnClickListener() {
@Override
public void onClick(View v) {
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this, true);
Intent newIntent = IntentUtils.newIntent(TrackRecordingActivity.this, TrackRecordedActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
updateMenuItems(true);
finish();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -185,7 +154,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
}
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, false, recordListener, stopListener);
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, false, this);
}
@Override
@@ -363,6 +332,32 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils);
}
@Override
public void recordStart() {
if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
}
@Override
public void recordStop() {
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this, true);
Intent newIntent = IntentUtils.newIntent(TrackRecordingActivity.this, TrackRecordedActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
updateMenuItems(true);
finish();
}
private class CustomFragmentPagerAdapter extends FragmentPagerAdapter {
public CustomFragmentPagerAdapter(@NonNull FragmentManager fm, int behavior) {
@@ -0,0 +1,158 @@
package de.dennisguse.opentracks.adapters;
import android.content.Context;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.sensor.SensorData;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataCyclingPower;
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
import de.dennisguse.opentracks.util.StringUtils;
public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public static final int HEART_RATE_TYPE = 0;
public static final int CADENCE_TYPE = 1;
public static final int POWER_TYPE = 2;
private List<Pair<Integer, SensorData>> sensorDataList;
private final Context context;
public SensorsAdapter(Context context) {
this.context = context;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.sensor_item, parent, false);
return new SensorsAdapter.ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
SensorsAdapter.ViewHolder viewHolder = (SensorsAdapter.ViewHolder) holder;
int type = sensorDataList.get(position).first;
SensorData sensorData = sensorDataList.get(position).second;
viewHolder.setData(sensorData, type);
}
@Override
public int getItemCount() {
if (sensorDataList == null) {
return 0;
} else {
return sensorDataList.size();
}
}
@Override
public int getItemViewType(int position) {
return sensorDataList.get(position).first;
}
public List<Pair<Integer, SensorData>> swapData(List<Pair<Integer, SensorData>> data) {
if (sensorDataList == data) {
return null;
}
sensorDataList = data;
if (data != null) {
this.notifyDataSetChanged();
}
return data;
}
private class ViewHolder extends RecyclerView.ViewHolder {
TextView label;
TextView sensorValue;
TextView value;
TextView unit;
public ViewHolder(@NonNull View itemView) {
super(itemView);
label = itemView.findViewById(R.id.stats_sensor_label);
sensorValue = itemView.findViewById(R.id.stats_sensor_sensor_value);
value = itemView.findViewById(R.id.stats_sensor_value);
unit = itemView.findViewById(R.id.stats_sensor_unit);
}
public void setData(SensorData sensorData, int type) {
switch (type) {
case HEART_RATE_TYPE:
setHeartRateSensorData((SensorDataHeartRate) sensorData);
break;
case CADENCE_TYPE:
setCadenceSensorData((SensorDataCycling.Cadence) sensorData);
break;
case POWER_TYPE:
setPowerSensorData((SensorDataCyclingPower) sensorData);
break;
default:
throw new RuntimeException("Unknown sensor type");
}
}
private void setHeartRateSensorData(SensorDataHeartRate data) {
String sensorValue = context.getString(R.string.value_unknown);
String sensorName = context.getString(R.string.value_unknown);
if (data != null) {
sensorName = data.getSensorNameOrAddress();
if (data.hasHeartRate_bpm() && data.isRecent()) {
sensorValue = StringUtils.formatDecimal(data.getHeartRate_bpm(), 0);
}
}
this.label.setText(context.getString(R.string.sensor_state_heart_rate));
this.sensorValue.setText(sensorName);
this.value.setText(sensorValue);
this.unit.setText(R.string.sensor_unit_beats_per_minute);
}
private void setCadenceSensorData(SensorDataCycling.Cadence data) {
String sensorValue = context.getString(R.string.value_unknown);
String sensorName = context.getString(R.string.value_unknown);
if (data != null) {
sensorName = data.getSensorNameOrAddress();
if (data.hasCadence_rpm() && data.isRecent()) {
sensorValue = StringUtils.formatDecimal(data.getCadence_rpm(), 0);
}
}
this.label.setText(context.getString(R.string.sensor_state_cadence));
this.sensorValue.setText(sensorName);
this.value.setText(sensorValue);
this.unit.setText(R.string.sensor_unit_rounds_per_minute);
}
private void setPowerSensorData(SensorDataCyclingPower data) {
String sensorValue = context.getString(R.string.value_unknown);
String sensorName = context.getString(R.string.value_unknown);
if (data != null) {
sensorName = data.getSensorNameOrAddress();
if (data.hasPower_w() && data.isRecent()) {
sensorValue = StringUtils.formatDecimal(data.getPower_w(), 0);
}
}
this.label.setText(context.getString(R.string.sensor_state_power));
this.sensorValue.setText(sensorName);
this.value.setText(sensorValue);
this.unit.setText(R.string.sensor_unit_power);
}
}
}
@@ -1,425 +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.content;
import android.database.Cursor;
import android.location.Location;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.SortedSet;
import java.util.TreeSet;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* Engine for searching for tracks and markers by text.
*
* @author Rodrigo Damazio
*/
public class SearchEngine {
/**
* WHERE query to get tracks by name.
*/
private static final String TRACK_SELECTION_QUERY =
TracksColumns.NAME + " LIKE ? OR " +
TracksColumns.DESCRIPTION + " LIKE ? OR " +
TracksColumns.CATEGORY + " LIKE ?";
/**
* WHERE query to get markers by name.
*/
private static final String MARKER_SELECTION_QUERY =
MarkerColumns.NAME + " LIKE ? OR " +
MarkerColumns.DESCRIPTION + " LIKE ? OR " +
MarkerColumns.CATEGORY + " LIKE ?";
/**
* Order of track results.
*/
private static final String TRACK_SELECTION_ORDER = TracksColumns._ID + " DESC LIMIT 1000";
/**
* Order of marker results.
*/
private static final String MARKER_SELECTION_ORDER = MarkerColumns._ID + " DESC";
/**
* How much we promote a match in the track category.
*/
private static final double TRACK_CATEGORY_PROMOTION = 2.0;
/**
* How much we promote a match in the track description.
*/
private static final double TRACK_DESCRIPTION_PROMOTION = 8.0;
/**
* How much we promote a match in the track name.
*/
private static final double TRACK_NAME_PROMOTION = 16.0;
/**
* How much we promote a marker result if it's in the currently-selected track.
*/
private static final double CURRENT_TRACK_MARKER_PROMOTION = 2.0;
/**
* How much we promote a track result if it's the currently-selected track.
*/
private static final double CURRENT_TRACK_DEMOTION = 0.5;
/**
* Maximum number of markers which will be retrieved and scored.
*/
private static final int MAX_SCORED_MARKERS = 100;
/**
* Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000)
*/
public static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L;
/**
* Comparator for scored results.
*/
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR = (r1, r2) -> {
// Score ordering.
int scoreDiff = Double.compare(r2.score, r1.score);
if (scoreDiff != 0) {
return scoreDiff;
}
// Make tracks come before markers.
if (r1.marker != null && r2.track != null) {
return 1;
} else if (r1.track != null && r2.marker != null) {
return -1;
}
// Finally, use arbitrary ordering, by ID.
long id1 = r1.track != null ? r1.track.getId().getId() : r1.marker.getId().getId();
long id2 = r2.track != null ? r2.track.getId().getId() : r2.marker.getId().getId();
long idDiff = id2 - id1;
return Long.signum(idDiff);
};
private final ContentProviderUtils providerUtils;
public SearchEngine(ContentProviderUtils providerUtils) {
this.providerUtils = providerUtils;
}
/**
* Squashes a number by calculating 1 / log (1 + x).
*/
//TODO Why is this done?
private static double squash(double x) {
return 1.0 / Math.log1p(x);
}
/**
* Executes a searchable query and returns a set of sorted results.
*
* @param query the query to execute
* @return a set of results, sorted according to their score
*/
public SortedSet<ScoredResult> search(SearchQuery query) {
TreeSet<ScoredResult> scoredResults = new TreeSet<>(SCORED_RESULT_COMPARATOR);
scoreTrackResults(retrieveTracks(query), query, scoredResults);
scoreMarkersResults(retrieveMarkers(query), query, scoredResults);
return scoredResults;
}
/**
* Retrieves tracks matching the given query from the database.
*
* @param query the query to retrieve for
*/
private List<Track> retrieveTracks(SearchQuery query) {
ArrayList<Track> tracks = new ArrayList<>();
String queryLikeSelection = "%" + query.textQuery + "%";
String[] trackSelectionArgs = new String[]{
queryLikeSelection,
queryLikeSelection,
queryLikeSelection
};
try (Cursor cursor = providerUtils.getTrackCursor(TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER)) {
if (cursor != null) {
tracks.ensureCapacity(cursor.getCount());
while (cursor.moveToNext()) {
tracks.add(providerUtils.createTrack(cursor));
}
}
}
return tracks;
}
/**
* Retrieves markers matching the given query from the database.
*
* @param query the query to retrieve for
*/
private List<Marker> retrieveMarkers(SearchQuery query) {
ArrayList<Marker> markers = new ArrayList<>();
String queryLikeSelection2 = "%" + query.textQuery + "%";
String[] markersSelectionArgs = new String[]{
queryLikeSelection2,
queryLikeSelection2,
queryLikeSelection2
};
try (Cursor cursor = providerUtils.getMarkerCursor(MARKER_SELECTION_QUERY, markersSelectionArgs, MARKER_SELECTION_ORDER, MAX_SCORED_MARKERS)) {
if (cursor != null) {
markers.ensureCapacity(cursor.getCount());
while (cursor.moveToNext()) {
Marker marker = providerUtils.createMarker(cursor);
if (LocationUtils.isValidLocation(marker.getLocation())) {
markers.add(marker);
}
}
}
}
return markers;
}
/**
* Scores a collection of track results.
*
* @param tracks the results to score
* @param query the query to score for
* @param output the collection to fill with scored results
*/
private void scoreTrackResults(Collection<Track> tracks, SearchQuery query, Collection<ScoredResult> output) {
for (Track track : tracks) {
// Calculate the score.
double score = scoreTrackResult(query, track);
// Add to the output.
output.add(new ScoredResult(track, score));
}
}
/**
* Scores a single track result.
*
* @param query the query to score for
* @param track the results to score
* @return the score for the track
*/
private double scoreTrackResult(SearchQuery query, Track track) {
double score = 1.0;
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
TrackStatistics statistics = track.getTrackStatistics();
long meanTimestamp = (statistics.getStartTime_ms() + statistics.getStopTime_ms()) / 2L;
score *= getTimeBoost(query, meanTimestamp);
// Score the currently-selected track lower (user is already there, wouldn't be searching for it).
if (track.getId().equals(query.currentTrackId)) {
score *= CURRENT_TRACK_DEMOTION;
}
return score;
}
/**
* Scores a collection of marker results.
*
* @param markers the results to score
* @param query the query to score for
* @param output the collection to fill with scored results
*/
private void scoreMarkersResults(Collection<Marker> markers, SearchQuery query, Collection<ScoredResult> output) {
for (Marker marker : markers) {
// Calculate the score.
double score = scoreMarkerResult(query, marker);
// Add to the output.
output.add(new ScoredResult(marker, score));
}
}
/**
* Scores a single marker result.
*
* @param query the query to score for
* @param marker the results to score
* @return the score for the marker
*/
private double scoreMarkerResult(SearchQuery query, Marker marker) {
double score = 1.0;
Location location = marker.getLocation();
score *= getTitleBoost(query, marker.getName(), marker.getDescription(), marker.getCategory());
score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude());
score *= getTimeBoost(query, location.getTime());
// Score markers in the currently-selected track higher (searching inside the current track).
if (marker.getTrackId() != null && marker.getTrackId().equals(query.currentTrackId)) {
score *= CURRENT_TRACK_MARKER_PROMOTION;
}
return score;
}
/**
* Calculates the boosting of the score due to the field(s) in which the match occured.
*
* @param query the query to boost for
* @param name the name of the track or marker
* @param description the description of the track or marker
* @param category the category of the track or marker
* @return the total boost to be applied to the result
*/
private double getTitleBoost(SearchQuery query, String name, String description, String category) {
// Title boost: track name > description > category.
double boost = 1.0;
if (name.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
boost *= TRACK_NAME_PROMOTION;
}
if (description.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
boost *= TRACK_DESCRIPTION_PROMOTION;
}
if (category.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
boost *= TRACK_CATEGORY_PROMOTION;
}
return boost;
}
/**
* Calculates the boosting of the score due to the recency of the matched entity.
*
* @param query the query to boost for
* @param timestamp the timestamp to calculate the boost for
* @return the total boost to be applied to the result
*/
private double getTimeBoost(SearchQuery query, long timestamp) {
if (timestamp < OLDEST_ALLOWED_TIMESTAMP) {
// Safety: if timestamp is too old or invalid, don't rank based on time.
return 1.0;
}
// Score recent tracks higher.
long timeAgoHours = (long) ((query.currentTimestamp - timestamp) * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN * UnitConversions.MIN_TO_HR);
if (timeAgoHours > 0L) {
return squash(timeAgoHours);
} else {
// Should rarely happen (track recorded in the last hour).
return Double.POSITIVE_INFINITY;
}
}
/**
* Calculates the boosting of the score due to proximity to a location.
*
* @param query the query to boost for
* @param latitude the latitude to calculate the boost for
* @param longitude the longitude to calculate the boost for
* @return the total boost to be applied to the result
*/
private double getDistanceBoost(SearchQuery query, double latitude, double longitude) {
if (query.currentLocation == null) {
return 1.0;
}
float[] distanceResults = new float[1];
Location.distanceBetween(
latitude, longitude,
query.currentLocation.getLatitude(), query.currentLocation.getLongitude(),
distanceResults);
// Score tracks close to the current location higher.
double distanceKm = distanceResults[0] * UnitConversions.M_TO_KM;
if (distanceKm > 0.0) {
// Use the inverse of the amortized distance.
return squash(distanceKm);
} else {
// Should rarely happen (distance is exactly 0).
return Double.POSITIVE_INFINITY;
}
}
/**
* Description of a searchable query, along with all contextual data needed to execute it.
*/
public static class SearchQuery {
final String textQuery;
final Location currentLocation;
final Track.Id currentTrackId;
final long currentTimestamp;
public SearchQuery(String textQuery, Location currentLocation, Track.Id currentTrackId, long currentTimestamp) {
this.textQuery = textQuery.toLowerCase(Locale.getDefault());
this.currentLocation = currentLocation;
this.currentTrackId = currentTrackId;
this.currentTimestamp = currentTimestamp;
}
}
/**
* Description of a searchable result which has been retrieved and scored.
*/
public static class ScoredResult {
public final Track track;
public final Marker marker;
final double score;
ScoredResult(Track track, double score) {
this.track = track;
this.marker = null;
this.score = score;
}
ScoredResult(Marker marker, double score) {
this.track = null;
this.marker = marker;
this.score = score;
}
@NonNull
@Override
public String toString() {
return "ScoredResult ["
+ (track != null ? ("trackId=" + track.getId().getId() + ", ") : "")
+ (marker != null ? ("wptId=" + marker.getId().getId() + ", ") : "")
+ "score=" + score + "]";
}
}
}
@@ -118,6 +118,10 @@ public class Track {
this.id = id;
}
protected Id(Parcel in) {
id = in.readLong();
}
//TOOD Limit visibility to TrackRecordingService / ContentProvider
public long getId() {
return id;
@@ -28,8 +28,6 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import java.io.File;
import java.util.ArrayList;
@@ -415,8 +413,7 @@ public class ContentProviderUtils {
if (!markerId.isValid()) {
return null;
}
try (Cursor cursor = getMarkerCursor(null, MarkerColumns._ID + "=?",
new String[]{Long.toString(markerId.getId())}, MarkerColumns._ID, 1)) {
try (Cursor cursor = getMarkerCursor(null, MarkerColumns._ID + "=?", new String[]{Long.toString(markerId.getId())}, MarkerColumns._ID, 1)) {
if (cursor != null && cursor.moveToFirst()) {
return createMarker(cursor);
}
@@ -424,18 +421,6 @@ public class ContentProviderUtils {
return null;
}
/**
* The caller owns the returned cursor and is responsible for closing it.
*
* @param selection the selection. Can be null
* @param selectionArgs the selection arguments. Can be null
* @param sortOrder the sort order. Can be null
* @param maxCount the maximum number of markers to return. -1 for no limit
*/
public Cursor getMarkerCursor(String selection, String[] selectionArgs, String sortOrder, int maxCount) {
return getMarkerCursor(null, selection, selectionArgs, sortOrder, maxCount);
}
/**
* The caller owns the returned cursor and is responsible for closing it.
*
@@ -474,21 +459,6 @@ public class ContentProviderUtils {
return markers;
}
public static Loader<Cursor> getMarkersLoader(Context context, @Nullable Track.Id trackId) {
final String[] PROJECTION = new String[]{MarkerColumns._ID,
MarkerColumns.NAME, MarkerColumns.DESCRIPTION, MarkerColumns.CATEGORY,
MarkerColumns.TIME, MarkerColumns.PHOTOURL,
MarkerColumns.LATITUDE, MarkerColumns.LONGITUDE};
if (trackId != null) {
return new CursorLoader(context, MarkerColumns.CONTENT_URI, PROJECTION,
MarkerColumns.TRACKID + "=?",
new String[]{String.valueOf(trackId.getId())}, null);
} else {
return new CursorLoader(context, MarkerColumns.CONTENT_URI, PROJECTION, null, null, null);
}
}
@Deprecated //TODO TracksColumns.MARKER_COUNT while querying for tracks
public int getMarkerCount(Track.Id trackId) {
if (!trackId.isValid()) {
@@ -42,7 +42,7 @@ public final class SensorDataCycling {
* Workaround for Wahoo CADENCE: provides speed instead of cadence
*/
public Cadence(@NonNull SensorDataCycling.Speed speed) {
this(speed.getSensorAddress(), speed.getSensorName(), speed.getWheelRevolutionsCount(), speed.getWheelRevolutionsTime());
this(speed.getSensorAddress(), speed.getSensorName(), speed.wheelRevolutionsCount, speed.wheelRevolutionsTime);
}
public boolean hasData() {
@@ -82,7 +82,7 @@ public final class SensorDataCycling {
@NonNull
@Override
public String toString() {
return "cadence=" + getCadence_rpm() + " time=" + getCrankRevolutionsTime() + " count=" + getCrankRevolutionsCount();
return "cadence=" + cadence_rpm + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
}
@Override
@@ -90,7 +90,11 @@ public final class SensorDataCycling {
if (!(obj instanceof Cadence)) return false;
Cadence comp = (Cadence) obj;
return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime();
if (hasData() && comp.hasData() == hasData()) {
return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime();
} else {
return false;
}
}
}
@@ -149,7 +153,7 @@ public final class SensorDataCycling {
@NonNull
@Override
public String toString() {
return "speed=" + getSpeed_mps() + " time=" + getWheelRevolutionsTime() + " count=" + getWheelRevolutionsCount();
return "speed=" + speed_mps + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
}
@Override
@@ -157,7 +161,11 @@ public final class SensorDataCycling {
if (!(obj instanceof Speed)) return false;
Speed comp = (Speed) obj;
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
if (hasData() && comp.hasData() == hasData()) {
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
} else {
return false;
}
}
}
@@ -0,0 +1,32 @@
package de.dennisguse.opentracks.content.sensor;
import androidx.annotation.NonNull;
public class SensorDataCyclingPower extends SensorData {
private final Float power_w;
public SensorDataCyclingPower(String address) {
super(address);
this.power_w = null;
}
public SensorDataCyclingPower(String name, String address, float power_w) {
super(name, address);
this.power_w = power_w;
}
public boolean hasPower_w() {
return power_w != null;
}
public float getPower_w() {
return power_w;
}
@NonNull
@Override
public String toString() {
return "power=" + power_w;
}
}
@@ -12,6 +12,8 @@ public final class SensorDataSet {
private SensorDataCycling.Speed cyclingSpeed;
private SensorDataCyclingPower cyclingPower;
public SensorDataSet() {
}
@@ -27,6 +29,10 @@ public final class SensorDataSet {
return cyclingSpeed;
}
public SensorDataCyclingPower getCyclingPower() {
return cyclingPower;
}
public void set(SensorData data) {
set(data, data);
}
@@ -39,6 +45,7 @@ public final class SensorDataSet {
this.heartRate = null;
this.cyclingCadence = null;
this.cyclingSpeed = null;
this.cyclingPower = null;
}
public void fillTrackPoint(TrackPoint trackPoint) {
@@ -53,6 +60,10 @@ public final class SensorDataSet {
if (cyclingSpeed != null && cyclingSpeed.hasSpeed_mps()) {
trackPoint.setSpeed(cyclingSpeed.getSpeed_mps());
}
if (cyclingPower != null && cyclingPower.hasPower_w()) {
trackPoint.setPower(cyclingPower.getPower_w());
}
}
@NonNull
@@ -60,10 +71,11 @@ public final class SensorDataSet {
public String toString() {
return (getHeartRate() != null ? "" + getHeartRate() : "")
+ (getCyclingCadence() != null ? " " + getCyclingCadence() : "")
+ (getCyclingSpeed() != null ? " " + getCyclingSpeed() : "");
+ (getCyclingSpeed() != null ? " " + getCyclingSpeed() : "")
+ (getCyclingPower() != null ? " " + getCyclingPower() : "");
}
private void set(SensorData type, SensorData data) {
private void set(@NonNull SensorData type, SensorData data) {
if (type instanceof SensorDataHeartRate) {
this.heartRate = (SensorDataHeartRate) data;
return;
@@ -78,6 +90,11 @@ public final class SensorDataSet {
return;
}
throw new UnsupportedOperationException();
if (type instanceof SensorDataCyclingPower) {
this.cyclingPower = (SensorDataCyclingPower) data;
return;
}
throw new UnsupportedOperationException(type.getClass().getCanonicalName());
}
}
@@ -76,7 +76,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
private ChartBinding viewBinding;
//TODO Why is this needed?
//TODO REMOVE: Used to restore the TrackStatistics via TrackStatisticsUpdater; for the diagrams we should consider not this setting and just use all data that is in the database
@Deprecated
private int recordingDistanceInterval;
private String category = "";
@@ -65,24 +65,18 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Marker.Id[] markerIds = (Marker.Id[]) getArguments().getParcelableArray(KEY_MARKER_IDS);
final Context context = getContext();
final FragmentActivity fragmentActivity = getActivity();
int titleId;
int messageId;
if (markerIds == null) {
titleId = R.string.generic_delete_all_confirm_title;
messageId = R.string.marker_delete_all_confirm_message;
} else {
titleId = markerIds.length > 1 ? R.string.generic_delete_selected_confirm_title : R.string.marker_delete_one_confirm_title;
messageId = markerIds.length > 1 ? R.string.marker_delete_multiple_confirm_message : R.string.marker_delete_one_confirm_message;
}
int titleId = markerIds.length > 1 ? R.string.generic_delete_selected_confirm_title : R.string.marker_delete_one_confirm_title;
int messageId = markerIds.length > 1 ? R.string.marker_delete_multiple_confirm_message : R.string.marker_delete_one_confirm_message;
return DialogUtils.createConfirmationDialog(
fragmentActivity, titleId, getString(messageId), (dialog, which) -> new Thread(() -> {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(fragmentActivity);
for (Marker.Id markerId : markerIds) {
contentProviderUtils.deleteMarker(getContext(), markerId);
contentProviderUtils.deleteMarker(context, markerId);
}
caller.onDeleteMarkerDone();
caller.onMarkerDeleted();
}).start());
}
@@ -96,6 +90,6 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
/**
* Called when delete marker is done.
*/
void onDeleteMarkerDone();
void onMarkerDeleted();
}
}
@@ -176,12 +176,16 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
@Override
public void onTrackUpdated(Track track) {
if (isResumed()) {
// Set category.
category = track != null ? track.getCategory() : "";
getActivity().runOnUiThread(() -> {
if (isResumed()) {
// Set category.
category = track != null ? track.getCategory() : "";
// Set rate label.
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
viewBinding.intervalRate.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
// Set rate label.
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
viewBinding.intervalRate.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
}
});
}
}
@@ -5,24 +5,28 @@ import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.adapters.SensorsAdapter;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorData;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
@@ -60,6 +64,9 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
private StatisticsRecordingBinding viewBinding;
private SensorsAdapter sensorsAdapter;
private RecyclerView sensorsRecyclerView;
public static Fragment newInstance() {
return new StatisticsRecordingFragment();
}
@@ -101,19 +108,13 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
handlerUpdateUI = new Handler();
viewBinding.statsActivityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), ""));
viewBinding.statsActivityTypeIcon.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
((TrackRecordingActivity) getActivity()).chooseActivityType(category);
}
return true;
});
viewBinding.statsActivityTypeIcon.setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
((TrackRecordingActivity) getActivity()).chooseActivityType(category);
}
return true;
});
viewBinding.statsActivityTypeIcon.setOnClickListener(v -> ((TrackRecordingActivity) getActivity()).chooseActivityType(category));
sensorsAdapter = new SensorsAdapter(getContext());
sensorsRecyclerView = viewBinding.statsSensorsRecyclerView;
sensorsRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
sensorsRecyclerView.setAdapter(sensorsAdapter);
}
@Override
@@ -271,55 +272,23 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
} else {
SensorDataSet sensorDataSet = trackRecordingService.getSensorData();
if (sensorDataSet != null) {
setHeartRateSensorData(sensorDataSet);
setCadenceSensorData(sensorDataSet);
setSpeedSensorData(sensorDataSet, isSelectedTrackRecording());
List<Pair<Integer, SensorData>> sensorDataList = new ArrayList<>();
if (sensorDataSet.getHeartRate() != null) {
sensorDataList.add(new Pair<>(SensorsAdapter.HEART_RATE_TYPE, sensorDataSet.getHeartRate()));
}
if (sensorDataSet.getCyclingCadence() != null) {
sensorDataList.add(new Pair<>(SensorsAdapter.CADENCE_TYPE, sensorDataSet.getCyclingCadence()));
}
if(sensorDataSet.getCyclingPower() != null) {
sensorDataList.add(new Pair<>(SensorsAdapter.POWER_TYPE, sensorDataSet.getCyclingPower()));
}
sensorsAdapter.swapData(sensorDataList);
setSpeedSensorData(sensorDataSet);
}
setTotalElevationGain(trackRecordingService.getElevationGain_m());
}
}
private void setHeartRateSensorData(SensorDataSet sensorDataSet) {
int isVisible = sensorDataSet.getHeartRate() != null ? View.VISIBLE : View.GONE;
viewBinding.statsSensorHeartRateGroup.setVisibility(isVisible);
setVisibilitySensorHorizontalLine();
String sensorValue = getContext().getString(R.string.value_unknown);
String sensorName = getContext().getString(R.string.value_unknown);
if (sensorDataSet.getHeartRate() != null) {
SensorDataHeartRate data = sensorDataSet.getHeartRate();
sensorName = data.getSensorNameOrAddress();
if (data.hasHeartRate_bpm() && data.isRecent()) {
sensorValue = StringUtils.formatDecimal(data.getHeartRate_bpm(), 0);
}
}
viewBinding.statsSensorHeartRateSensorValue.setText(sensorName);
viewBinding.statsSensorHeartRateValue.setText(sensorValue);
}
private void setCadenceSensorData(SensorDataSet sensorDataSet) {
int isVisible = sensorDataSet.getCyclingCadence() != null ? View.VISIBLE : View.GONE;
viewBinding.statsSensorCadenceGroup.setVisibility(isVisible);
setVisibilitySensorHorizontalLine();
String sensorValue = getContext().getString(R.string.value_unknown);
String sensorName = getContext().getString(R.string.value_unknown);
if (sensorDataSet.getCyclingCadence() != null) {
SensorDataCycling.Cadence data = sensorDataSet.getCyclingCadence();
sensorName = data.getSensorNameOrAddress();
if (data.hasCadence_rpm() && data.isRecent()) {
sensorValue = StringUtils.formatDecimal(data.getCadence_rpm(), 0);
}
}
viewBinding.statsSensorCadenceSensorValue.setText(sensorName);
viewBinding.statsSensorCadenceValue.setText(sensorValue);
}
// Set elevation gain
private void setTotalElevationGain(Float elevationGain_m) {
//TODO Check if we can distribute the total elevation gain via trackStatistics instead of doing some computation in the UI layer.
@@ -340,23 +309,12 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
viewBinding.statsElevationGainUnit.setText(parts.second);
}
/**
* If cadence and hear rate groups are invisible then sensor horizontal line hast to be invisible too.
*/
private void setVisibilitySensorHorizontalLine() {
if (viewBinding.statsSensorCadenceGroup.getVisibility() != View.VISIBLE && viewBinding.statsSensorHeartRateGroup.getVisibility() != View.VISIBLE) {
viewBinding.statsSensorHorizontalLine.setVisibility(View.GONE);
}
}
private void setSpeedSensorData(SensorDataSet sensorDataSet) {
if (sensorDataSet != null && sensorDataSet.getCyclingSpeed() != null) {
SensorDataCycling.Speed data = sensorDataSet.getCyclingSpeed();
private void setSpeedSensorData(SensorDataSet sensorDataSet, boolean isRecording) {
if (isRecording) {
if (sensorDataSet != null && sensorDataSet.getCyclingSpeed() != null) {
SensorDataCycling.Speed data = sensorDataSet.getCyclingSpeed();
if (data.hasSpeed_mps() && data.isRecent()) {
setSpeed(data.getSpeed_mps());
}
if (data.hasSpeed_mps() && data.isRecent()) {
setSpeed(data.getSpeed_mps());
}
}
}
@@ -380,7 +338,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set activity type
{
viewBinding.statsActivityTypeIcon.setEnabled(isRecording);
TrackIconUtils.setIconSpinner(viewBinding.statsActivityTypeIcon, trackIconValue);
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
}
// Set time
@@ -24,7 +24,6 @@ import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
@@ -68,7 +68,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
private final ContentProviderUtils contentProviderUtils;
private final int recordingDistanceInterval;
private Track.Id importTrackId;
private final List<Track.Id> trackIds = new ArrayList<>();
private final List<Marker> markers = new ArrayList<>();
@@ -104,11 +103,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
this.recordingDistanceInterval = PreferencesUtils.getRecordingDistanceInterval(context);
}
AbstractFileTrackImporter(Context context, ContentProviderUtils contentProviderUtils, Track.Id importTrackId) {
this(context, contentProviderUtils);
this.importTrackId = importTrackId;
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
@@ -181,6 +175,10 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
// No more markers
return;
}
// If marker had photo it must be translated to internal photo url (depend on track id)
if (marker.hasPhoto()) {
marker.setPhotoUrl(getInternalPhotoUrl(marker.getPhotoUrl()));
}
}
if (trackPoint == null) {
@@ -230,17 +228,8 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
*/
protected void onTrackStart() throws SAXException {
trackData = new TrackData();
Track.Id trackId;
if (importTrackId == null) {
Uri uri = contentProviderUtils.insertTrack(trackData.track);
trackId = new Track.Id(Long.parseLong(uri.getLastPathSegment()));
} else {
if (trackIds.size() > 0) {
throw new SAXException(createErrorMessage("Cannot import more than one track to an existing track " + importTrackId.getId()));
}
trackId = importTrackId;
contentProviderUtils.clearTrack(trackId);
}
Uri uri = contentProviderUtils.insertTrack(trackData.track);
Track.Id trackId = new Track.Id(Long.parseLong(uri.getLastPathSegment()));
trackIds.add(trackId);
trackData.track.setId(trackId);
}
@@ -401,7 +390,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* @param externalPhotoUrl the file name
*/
protected String getInternalPhotoUrl(String externalPhotoUrl) {
if (importTrackId == null) {
if (trackData.track.getId() == null) {
Log.e(TAG, "Track id is invalid.");
return null;
}
@@ -412,7 +401,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
}
String importFileName = KmzTrackImporter.importNameForFilename(externalPhotoUrl);
File file = FileUtils.getPhotoFileIfExists(context, importTrackId, Uri.parse(importFileName));
File file = FileUtils.buildInternalPhotoFile(context, trackData.track.getId(), Uri.parse(importFileName));
if (file != null) {
Uri photoUri = FileUtils.getUriForFile(context, file);
return "" + photoUri;
@@ -110,7 +110,7 @@ public class ImportActivity extends FragmentActivity {
@Override
public void onBackPressed() {
if (doubleBackToCancel || getTotalDone() == summary.getTotalCount()) {
if (doubleBackToCancel || (summary != null && getTotalDone() == summary.getTotalCount())) {
super.onBackPressed();
viewModel.cancel();
getViewModelStore().clear();
@@ -130,7 +130,7 @@ public class ImportActivity extends FragmentActivity {
}
private int getTotalDone() {
return summary.getSuccessCount() + summary.getExistsCount() + summary.getErrorCount();
return summary != null ? summary.getSuccessCount() + summary.getExistsCount() + summary.getErrorCount() : 0;
}
private void setProgress() {
@@ -193,10 +193,6 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
if (!MARKER_STYLE.equals(markerType)) {
return;
}
// If there is photoUrl it has to be changed because that url in kml file is a relative path to the internal kmz file.
photoUrl = getInternalPhotoUrl(photoUrl);
addMarker();
}
@@ -32,6 +32,7 @@ import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
@@ -53,7 +54,6 @@ public class KmzTrackImporter implements TrackImporter {
private static final int BUFFER_SIZE = 4096;
private final Context context;
private Track.Id importTrackId; //TODO needed?
private final Uri uriKmzFile;
/**
@@ -67,22 +67,10 @@ public class KmzTrackImporter implements TrackImporter {
@Override
public Track.Id importFile(InputStream inputStream) {
Track.Id trackId;
Track.Id trackId = findAndParseKmlFile(inputStream);
if (!copyKmzImages()) {
cleanImport(context, importTrackId);
return null;
}
try {
trackId = findAndParseKmlFile(inputStream);
} catch (Exception e) {
cleanImport(context, importTrackId);
throw e;
}
if (trackId == null) {
cleanImport(context, importTrackId);
if (!copyKmzImages(trackId)) {
cleanImport(context, trackId);
return null;
}
@@ -96,7 +84,7 @@ public class KmzTrackImporter implements TrackImporter {
*
* @return false if there are errors or true otherwise.
*/
private boolean copyKmzImages() {
private boolean copyKmzImages(Track.Id trackId) {
try (InputStream inputStream = context.getContentResolver().openInputStream(uriKmzFile);
ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
ZipEntry zipEntry;
@@ -109,7 +97,7 @@ public class KmzTrackImporter implements TrackImporter {
String fileName = zipEntry.getName();
if (hasImageExtension(fileName)) {
readAndSaveImageFile(zipInputStream, importNameForFilename(fileName));
readAndSaveImageFile(zipInputStream, trackId, importNameForFilename(fileName));
}
zipInputStream.closeEntry();
@@ -175,7 +163,7 @@ public class KmzTrackImporter implements TrackImporter {
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
if (Thread.interrupted()) {
Log.d(TAG, "Thread interrupted");
return null;
throw new RuntimeException(context.getString(R.string.import_thread_interrupted));
}
String fileName = zipEntry.getName();
@@ -183,7 +171,7 @@ public class KmzTrackImporter implements TrackImporter {
trackId = parseKml(zipInputStream);
if (trackId == null) {
Log.d(TAG, "Unable to parse kml in kmz");
return null;
throw new RuntimeException(context.getString(R.string.import_unable_to_import_file, fileName));
}
}
@@ -285,12 +273,12 @@ public class KmzTrackImporter implements TrackImporter {
* @param zipInputStream the zip input stream
* @param fileName the file name
*/
private void readAndSaveImageFile(ZipInputStream zipInputStream, String fileName) throws IOException {
if (importTrackId == null || fileName.equals("")) {
private void readAndSaveImageFile(ZipInputStream zipInputStream, Track.Id trackId, String fileName) throws IOException {
if (trackId == null || fileName.equals("")) {
return;
}
File dir = FileUtils.getPhotoDir(context, importTrackId);
File dir = FileUtils.getPhotoDir(context, trackId);
File file = new File(dir, fileName);
try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
@@ -25,6 +25,8 @@ import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager.WakeLock;
import android.util.Log;
@@ -46,12 +48,14 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.io.file.exporter.ExportServiceResultReceiver;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.services.handlers.HandlerServer;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.services.sensors.ElevationSumManager;
import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.ExportUtils;
@@ -69,7 +73,7 @@ import de.dennisguse.opentracks.util.TrackPointUtils;
*
* @author Leif Hendrik Wilden
*/
public class TrackRecordingService extends Service implements HandlerServer.HandlerServerInterface {
public class TrackRecordingService extends Service implements HandlerServer.HandlerServerInterface, ExportServiceResultReceiver.Receiver {
private static final String TAG = TrackRecordingService.class.getSimpleName();
@@ -431,7 +435,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
}
}
ExportUtils.postWorkoutExport(this, track);
ExportUtils.postWorkoutExport(this, track, new ExportServiceResultReceiver(new Handler(), this));
endRecording(true);
}
@@ -751,4 +755,16 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
public GpsStatusValue getGpsStatus() {
return handlerServer.getGpsStatus();
}
@Override
public void onReceiveResult(final int resultCode, final Bundle resultData) {
Log.w(TAG, "onReceiveResult: " + resultCode);
if (resultCode != ExportServiceResultReceiver.RESULT_CODE_SUCCESS) {
Intent intent = new Intent(this, SettingsActivity.class);
intent.putExtra(SettingsActivity.EXTRAS_CHECK_EXPORT_DIRECTORY, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}
}
@@ -32,6 +32,7 @@ import java.util.UUID;
import de.dennisguse.opentracks.content.sensor.SensorData;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataCyclingPower;
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
import de.dennisguse.opentracks.util.BluetoothUtils;
@@ -88,8 +89,14 @@ public abstract class BluetoothConnectionManager {
// Register for updates.
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(BluetoothUtils.CLIENT_CHARACTERISTIC_CONFIG_UUID);
if (descriptor == null) {
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID characteristic not available; cannot request notifications for changed data.");
return;
}
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);
}
@Override
@@ -170,7 +177,7 @@ public abstract class BluetoothConnectionManager {
public static class CyclingCadence extends BluetoothConnectionManager {
CyclingCadence(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
}
@Override
@@ -201,7 +208,7 @@ public abstract class BluetoothConnectionManager {
public static class CyclingSpeed extends BluetoothConnectionManager {
CyclingSpeed(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
}
@Override
@@ -219,6 +226,25 @@ public abstract class BluetoothConnectionManager {
}
}
public static class CyclingPower extends BluetoothConnectionManager {
CyclingPower(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_POWER_UUID, BluetoothUtils.CYCLING_POWER_MEASUREMENT_CHAR_UUID, observer);
}
@Override
protected SensorData createEmptySensorData(String address) {
return new SensorDataCyclingPower(address);
}
@Override
protected SensorDataCyclingPower parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Integer cyclingPower = BluetoothUtils.parseCyclingPower(characteristic);
return cyclingPower != null ? new SensorDataCyclingPower(address, sensorName, cyclingPower) : null;
}
}
interface SensorDataObserver {
void onChanged(SensorData sensorData);
@@ -59,6 +59,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
private final BluetoothConnectionManager.HeartRate heartRate = new BluetoothConnectionManager.HeartRate(this);
private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
private final BluetoothConnectionManager.CyclingSpeed cyclingSpeed = new BluetoothConnectionManager.CyclingSpeed(this);
private final BluetoothConnectionManager.CyclingPower cyclingPower = new BluetoothConnectionManager.CyclingPower(this);
private final SensorDataSet sensorDataSet = new SensorDataSet();
@@ -82,6 +83,12 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
connect(cyclingSpeed, address);
}
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_power_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingPowerSensorAddress(context);
connect(cyclingPower, address);
}
}
};
@@ -102,6 +109,8 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
public synchronized void stop() {
heartRate.disconnect();
cyclingCadence.disconnect();
cyclingSpeed.disconnect();
cyclingPower.disconnect();
sensorDataSet.clear();
@@ -19,12 +19,10 @@ package de.dennisguse.opentracks.settings;
import android.content.Context;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Spinner;
import android.widget.ImageView;
import androidx.preference.DialogPreference;
import androidx.preference.PreferenceDialogFragmentCompat;
@@ -63,7 +61,7 @@ public class ActivityTypePreference extends DialogPreference {
public static class ActivityPreferenceDialog extends PreferenceDialogFragmentCompat {
private AutoCompleteTextView textView;
private Spinner spinner;
private ImageView iconView;
static ActivityPreferenceDialog newInstance(String preferenceKey) {
ActivityTypePreference.ActivityPreferenceDialog dialog = new ActivityTypePreference.ActivityPreferenceDialog();
@@ -87,30 +85,19 @@ public class ActivityTypePreference extends DialogPreference {
textView.setAdapter(adapter);
textView.setOnItemClickListener((parent, v, position, id) -> {
String iconValue = TrackIconUtils.getIconValue(context, (String) textView.getAdapter().getItem(position));
TrackIconUtils.setIconSpinner(spinner, iconValue);
updateIcon(iconValue);
});
textView.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
String iconValue = TrackIconUtils.getIconValue(context, textView.getText().toString());
TrackIconUtils.setIconSpinner(spinner, iconValue);
updateIcon(iconValue);
}
});
String iconValue = TrackIconUtils.getIconValue(context, category);
spinner = view.findViewById(R.id.activity_type_preference_spinner);
spinner.setAdapter(TrackIconUtils.getIconSpinnerAdapter(context, iconValue));
spinner.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
showIconSelectDialog();
}
return true;
});
spinner.setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
showIconSelectDialog();
}
return true;
});
iconView = view.findViewById(R.id.activity_type_preference_spinner);
iconView.setOnClickListener((it) -> showIconSelectDialog());
updateIcon(TrackIconUtils.getIconValue(context, category));
}
private void showIconSelectDialog() {
@@ -129,15 +116,13 @@ public class ActivityTypePreference extends DialogPreference {
}
}
/**
* Updates the value of the dialog.
*
* @param iconValue the icon value
*/
public void updateUI(String iconValue) {
TrackIconUtils.setIconSpinner(spinner, iconValue);
textView.setText(getActivity().getString(TrackIconUtils.getIconActivityType(iconValue)));
textView.clearFocus();
}
private void updateIcon(String iconValue) {
iconView.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
}
}
}
@@ -1,10 +1,12 @@
package de.dennisguse.opentracks.settings;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.documentfile.provider.DocumentFile;
@@ -20,6 +22,7 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingPowerPreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeSensorPreference;
import de.dennisguse.opentracks.util.ActivityUtils;
@@ -31,8 +34,10 @@ import de.dennisguse.opentracks.util.StringUtils;
public class SettingsActivity extends AppCompatActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, ResetDialogPreference.ResetCallback {
private static final String TAG = SettingsActivity.class.getSimpleName();
public static final String EXTRAS_CHECK_EXPORT_DIRECTORY = "Check Export Directory";
private PrefsFragment prefsFragment;
private boolean checkExportDirectory = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -44,9 +49,29 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
toolbar.setTitle(R.string.menu_settings);
setSupportActionBar(toolbar);
Intent intent = getIntent();
if (intent != null && intent.hasExtra(EXTRAS_CHECK_EXPORT_DIRECTORY)) {
checkExportDirectory = true;
}
onReset();
}
@Override
protected void onResume() {
if (checkExportDirectory) {
checkExportDirectory = false;
new AlertDialog.Builder(this)
.setIcon(R.drawable.ic_logo_24dp)
.setTitle(R.string.app_name)
.setMessage(R.string.export_error_post_workout)
.setNeutralButton(R.string.generic_ok, null)
.create()
.show();
}
super.onResume();
}
@Override
public void onChooseActivityTypeDone(String iconValue) {
prefsFragment.setDefaultActivity(iconValue);
@@ -132,6 +157,8 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.HEART_RATE_SERVICE_UUID);
} else if (preference instanceof BluetoothLeCyclingCadenceAndSpeedPreference) {
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID);
} else if (preference instanceof BluetoothLeCyclingPowerPreference) {
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.CYCLING_POWER_UUID);
}
if (dialogFragment != null) {
@@ -0,0 +1,23 @@
package de.dennisguse.opentracks.settings.bluetooth;
import android.content.Context;
import android.util.AttributeSet;
public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreference {
public BluetoothLeCyclingPowerPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public BluetoothLeCyclingPowerPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public BluetoothLeCyclingPowerPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BluetoothLeCyclingPowerPreference(Context context) {
super(context);
}
}
@@ -2,7 +2,6 @@ package de.dennisguse.opentracks.util;
import android.app.Activity;
import android.app.SearchManager;
import android.content.ComponentName;
import android.content.Context;
import android.os.Build;
import android.os.VibrationEffect;
@@ -23,7 +22,6 @@ import java.util.ArrayList;
import de.dennisguse.opentracks.ContextualActionModeCallback;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.SearchListActivity;
import de.dennisguse.opentracks.TrackController;
import static android.content.Context.VIBRATOR_SERVICE;
@@ -93,8 +91,8 @@ public class ActivityUtils {
final SearchView searchView = (SearchView) menuItem.getActionView();
SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
if (searchManager != null) {
//NOTE: for some reason activity.getComponentName() did not trigger the SearchListActivity
searchView.setSearchableInfo(searchManager.getSearchableInfo(new ComponentName(activity, SearchListActivity.class)));
//NOTE: Use searchManager.getSearchableInfo(new ComponentName(activity, SearchActivity.class)) if another activity should handle the search
searchView.setSearchableInfo(searchManager.getSearchableInfo(activity.getComponentName()));
} else {
Log.w(TAG, "Could not retrieve SearchManager.");
}
@@ -39,8 +39,11 @@ public class BluetoothUtils {
public static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
public static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
public static final UUID CYCLING_POWER_UUID = new UUID(0x181800001000L, 0x800000805f9b34fbL);
public static final UUID CYCLING_POWER_MEASUREMENT_CHAR_UUID = new UUID(0x2A6300001000L, 0x800000805f9b34fbL);
public static final UUID CYCLING_SPEED_CADENCE_SERVICE_UUID = new UUID(0x181600001000L, 0x800000805f9b34fbL);
public static final UUID CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID = new UUID(0x2A5B00001000L, 0x800000805f9b34fbL);
public static final UUID CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID = new UUID(0x2A5B00001000L, 0x800000805f9b34fbL);
private static final String TAG = BluetoothUtils.class.getSimpleName();
@@ -62,7 +65,7 @@ public class BluetoothUtils {
}
public static Integer parseHeartRate(BluetoothGattCharacteristic characteristic) {
//DOCUMENTATION https://www.bluetooth.com/specifications/gatt/characteristics/
//DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.heart_rate_measurement.xml
byte[] raw = characteristic.getValue();
if (raw.length == 0) {
return null;
@@ -79,10 +82,21 @@ public class BluetoothUtils {
return null;
}
public static Integer parseCyclingPower(BluetoothGattCharacteristic characteristic) {
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.cycling_power_measurement.xml
int valueLength = characteristic.getValue().length;
if (valueLength < 4) {
return null;
}
return characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, 2);
}
/**
* Documentation: https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=261449
*/
public static SensorDataCycling.CadenceAndSpeed parseCyclingCrankAndWheel(String address, String sensorName, @NonNull BluetoothGattCharacteristic characteristic) {
// DOCUMENTATION https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.csc_measurement.xml
int valueLength = characteristic.getValue().length;
if (valueLength == 0) {
return null;
@@ -17,18 +17,20 @@ import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.ExportService;
import de.dennisguse.opentracks.io.file.exporter.ExportServiceResultReceiver;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
public class ExportUtils {
private static final String TAG = ExportUtils.class.getSimpleName();
public static void postWorkoutExport(Context context, Track track) {
public static void postWorkoutExport(Context context, Track track, ExportServiceResultReceiver resultReceiver) {
if (PreferencesUtils.shouldInstantExportAfterWorkout(context)) {
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(context);
DocumentFile directory = PreferencesUtils.getDefaultExportDirectoryUri(context);
exportTrack(context, trackFileFormat, directory, track);
ExportService.enqueue(context, resultReceiver, track.getId(), trackFileFormat, directory.getUri());
}
}
@@ -36,6 +38,10 @@ public class ExportUtils {
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context);
Uri exportDocumentFileUri = getExportDocumentFileUri(context, track.getId(), trackFileFormat, directory);
if (exportDocumentFileUri == null) {
Log.e(TAG, "Couldn't create document file for export");
return false;
}
try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFileUri)) {
if (trackExporter.writeTrack(track, outputStream)) {
@@ -65,12 +71,7 @@ public class ExportUtils {
final ContentResolver resolver = context.getContentResolver();
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(directoryUri, DocumentsContract.getDocumentId(directoryUri));
try {
Cursor c = resolver.query(
childrenUri,
new String[] { DocumentsContract.Document.COLUMN_DISPLAY_NAME },
null,null, null
);
try (Cursor c = resolver.query(childrenUri, new String[]{DocumentsContract.Document.COLUMN_DISPLAY_NAME}, null, null, null)) {
while (c.moveToNext()) {
fileNames.add(c.getString(0));
}
@@ -85,7 +86,10 @@ public class ExportUtils {
String exportFileName = getExportFileNameByTrackId(trackId, trackFileFormat.getExtension());
Uri exportDocumentFileUri = findFile(context, directory.getUri(), exportFileName);
if (exportDocumentFileUri == null) {
exportDocumentFileUri = directory.createFile(trackFileFormat.getMimeType(), exportFileName).getUri();
final DocumentFile file = directory.createFile(trackFileFormat.getMimeType(), exportFileName);
if (file != null) {
exportDocumentFileUri = file.getUri();
}
}
return exportDocumentFileUri;
}
@@ -98,12 +102,8 @@ public class ExportUtils {
final ContentResolver resolver = context.getContentResolver();
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(directoryUri, DocumentsContract.getDocumentId(directoryUri));
try {
Cursor c = resolver.query(
childrenUri,
new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME },
null,null, null
);
try (Cursor c = resolver.query(childrenUri, new String[]{DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME}, null, null, null)) {
while (c.moveToNext()) {
final String documentId = c.getString(0);
final String documentName = c.getString(1);
@@ -20,6 +20,7 @@ import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.content.FileProvider;
import androidx.documentfile.provider.DocumentFile;
@@ -289,6 +290,30 @@ public class FileUtils {
return file;
}
/**
* Builds interval photo file object for fileNameUri Uri.
*
* @param context the Context.
* @param trackId the id of the Track.
* @param fileNameUri the file name uri.
* @return file object or null.
*/
public static File buildInternalPhotoFile(Context context, Track.Id trackId, @NonNull Uri fileNameUri) {
if (fileNameUri == null) {
Log.w(TAG, "URI object is null.");
return null;
}
String filename = fileNameUri.getLastPathSegment();
if (filename == null) {
Log.w(TAG, "External photo contains no filename.");
return null;
}
File dir = FileUtils.getPhotoDir(context, trackId);
return new File(dir, filename);
}
/**
* Delete the directory recursively.
*
@@ -262,6 +262,15 @@ public class PreferencesUtils {
return getInt(context, R.string.settings_sensor_bluetooth_cycling_speed_wheel_circumference_key, DEFAULT);
}
public static boolean isBluetoothCyclingPowerSensorAddressNone(Context context) {
return isBluetoothSensorAddressNone(context, getBluetoothCyclingPowerSensorAddress(context));
}
public static String getBluetoothCyclingPowerSensorAddress(Context context) {
return getString(context, R.string.settings_sensor_bluetooth_cycling_power_key, getBluetoothSensorAddressNone(context));
}
public static boolean shouldShowStatsOnLockscreen(Context context) {
final boolean STATS_SHOW_ON_LOCKSCREEN_DEFAULT = context.getResources().getBoolean(R.bool.stats_show_on_lockscreen_while_recording_default);
return getBoolean(context, R.string.stats_show_on_lockscreen_while_recording_key, STATS_SHOW_ON_LOCKSCREEN_DEFAULT);
@@ -18,10 +18,6 @@ package de.dennisguse.opentracks.util;
import android.content.Context;
import android.util.Pair;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.Spinner;
import androidx.annotation.NonNull;
@@ -207,35 +203,6 @@ public class TrackIconUtils {
return UNKNOWN;
}
public static void setIconSpinner(Spinner spinner, String iconValue) {
ArrayAdapter<StringBuilder> adapter = (ArrayAdapter<StringBuilder>) spinner.getAdapter();
StringBuilder stringBuilder = adapter.getItem(0);
stringBuilder.delete(0, stringBuilder.length());
stringBuilder.append(iconValue);
adapter.notifyDataSetChanged();
}
public static ArrayAdapter<StringBuilder> getIconSpinnerAdapter(final Context context, String iconValue) {
return new ArrayAdapter<StringBuilder>(context, android.R.layout.simple_spinner_item, new StringBuilder[]{new StringBuilder(iconValue)}) {
@NonNull
@Override
public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) {
ImageView imageView;
if (convertView != null) {
imageView = (ImageView) convertView;
} else {
imageView = new ImageView(getContext());
}
imageView.setImageResource(TrackIconUtils.getIconDrawable(getItem(position) + ""));
int padding = ResourceUtils.dpToPx(getContext(), 1);
imageView.setPaddingRelative(padding, padding, padding, -padding);
return imageView;
}
};
}
/**
* Returns true if the activity type is in the list.
*
@@ -19,7 +19,6 @@ package de.dennisguse.opentracks.util;
import android.content.Context;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
/**
@@ -29,8 +28,6 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
*/
public class TrackUtils {
public static final String TRACK_SORT_ORDER = TracksColumns.STARTTIME + " DESC";
private TrackUtils() {
}
@@ -1,5 +0,0 @@
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@integer/buttonDelayMillis"
android:propertyName="fillAlpha"
android:valueFrom="1.0"
android:valueTo="0.0" />
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_button_stop_disabled" android:state_enabled="false" />
<item android:drawable="@drawable/ic_button_stop_pressed" android:state_pressed="true" />
<item android:drawable="@drawable/ic_button_stop" />
</selector>
@@ -24,9 +24,4 @@
android:strokeWidth="4.82273865"
android:strokeAlpha="1"
android:strokeColor="#00000000" />
<path
android:name="fade_out_circle"
android:fillAlpha="0.0"
android:fillColor="#a31a1f"
android:pathData="M62.5,62.5m-0,-62a50,50 0,1 1,-0 123a50,50 0,1 1,-0 -123" />
</vector>
@@ -1,6 +0,0 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_button_pause">
<target
android:name="fade_out_circle"
android:animation="@animator/button_animator" />
</animated-vector>
@@ -1,41 +0,0 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:width="82dp"
android:height="82dp"
android:viewportWidth="125"
android:viewportHeight="125">
<path
android:fillAlpha="1"
android:fillColor="#00000000"
android:pathData="M2.516,63.008a60.239,59.73 134.879,1 0,119.968 -1.017a60.239,59.73 134.879,1 0,-119.968 1.017z"
android:strokeWidth="5.03194237"
android:strokeAlpha="1"
android:strokeColor="#555555" />
<path
android:fillAlpha="1"
android:fillColor="#484848"
android:pathData="M62.5,62.5m-50,0a50,50 0,1 1,100 0a50,50 0,1 1,-100 0"
android:strokeWidth="6.9172821"
android:strokeAlpha="1"
android:strokeColor="#00000000" />
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:fillType="nonZero"
android:pathData="M53.02,54.02h17.46v17.46h-17.46z"
android:strokeWidth="1.04023623"
android:strokeAlpha="1"
android:strokeColor="#414141" />
<path
android:name="fade_out_circle"
android:fillAlpha="0.0"
android:fillColor="#a31a1f"
android:pathData="M62.5,62.5m-0,-62a50,50 0,1 1,-0 123a50,50 0,1 1,-0 -123"></path>
</vector>
</aapt:attr>
<target
android:name="fade_out_circle"
android:animation="@animator/button_animator" />
</animated-vector>
@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="82dp"
android:height="82dp"
android:viewportWidth="125"
android:viewportHeight="125">
<path
android:fillColor="#00000000"
android:pathData="M2.516,63.009a60.239,59.73 134.856,1 0,119.967 -1.017a60.239,59.73 134.856,1 0,-119.967 1.017z"
android:strokeWidth="5.0319"
android:strokeColor="#555" />
<path android:pathData="M62.5,62.5m-0,-50a50,50 0,1 1,-0 100a50,50 0,1 1,-0 -100">
<aapt:attr name="android:fillColor">
<gradient
android:endX="62.5"
android:endY="112.5"
android:startX="62.5"
android:startY="12.5"
android:type="linear">
<item
android:color="#FF404040"
android:offset="0" />
<item
android:color="#FF555555"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#dbdbdb"
android:pathData="M53.02,54.02h17.46v17.46h-17.46z"
android:strokeWidth="1.0402"
android:strokeColor="#414141" />
</vector>
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
</vector>
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z" />
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<transition xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The drawables used here can be solid colors, gradients, shapes, images, etc. -->
<item android:drawable="@color/track_controller_background_normal" />
<item android:drawable="@color/track_controller_background_activated" />
</transition>
@@ -19,7 +19,7 @@ limitations under the License.
android:id="@+id/track_controller_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@color/track_controller_background"
android:background="@drawable/track_controller_background"
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp">
@@ -30,7 +30,8 @@ limitations under the License.
android:layout_height="0dp"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_record" />
android:contentDescription="@string/image_record"
android:src="@drawable/ic_button_record" />
<RelativeLayout
android:layout_width="match_parent"
@@ -62,7 +63,7 @@ limitations under the License.
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_stop"
android:src="@drawable/ic_button_stop_disabled" />
android:src="@drawable/button_stop" />
</LinearLayout>
</merge>
@@ -31,7 +31,7 @@ limitations under the License.
android:layout_weight="1"
android:hint="@string/track_edit_activity_type_hint" />
<Spinner
<ImageView
android:id="@+id/activity_type_preference_spinner"
android:layout_width="wrap_content"
android:layout_height="match_parent"
-32
View File
@@ -1,32 +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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar" />
<ListView
android:id="@+id/search_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/search_list_empty"
style="@style/ListEmptyTextView"
android:text="@string/search_list_empty_message" />
</LinearLayout>
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:id="@+id/stats_sensor_label"
style="@style/StatsLargeLabel"
tools:text="@string/sensor_state_heart_rate"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/stats_sensor_sensor_value"
style="@style/StatsTinyValue"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/stats_sensor_label"
tools:text="90" />
<TextView
android:id="@+id/stats_sensor_value"
style="@style/StatsLargeValue"
android:value="@string/value_unknown"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/stats_sensor_sensor_value"
tools:text="90" />
<TextView
android:id="@+id/stats_sensor_unit"
style="@style/StatsUnit"
tools:text="@string/sensor_unit_beats_per_minute"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/stats_sensor_value" />
</androidx.constraintlayout.widget.ConstraintLayout>
+5 -80
View File
@@ -68,7 +68,7 @@
app:layout_constraintRight_toLeftOf="@+id/guideline3"
app:layout_constraintTop_toTopOf="parent" />
<Spinner
<ImageView
android:id="@+id/stats_activity_type_icon"
style="@style/StatsLargeValue"
android:layout_width="wrap_content"
@@ -403,89 +403,14 @@
app:layout_constraintRight_toLeftOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@+id/stats_location_barrier" />
<!-- Heart rate -->
<androidx.constraintlayout.widget.Group
android:id="@+id/stats_sensor_heart_rate_group"
android:layout_width="wrap_content"
<!-- Sensors data -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/stats_sensors_recycler_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:constraint_referenced_ids="stats_sensor_heart_rate_label,stats_sensor_heart_rate_sensor_value,stats_sensor_heart_rate_value,stats_sensor_heart_rate_unit" />
<TextView
android:id="@+id/stats_sensor_heart_rate_label"
style="@style/StatsLargeLabel"
android:text="@string/sensor_state_heart_rate"
app:layout_constrainedWidth="true"
app:layout_constraintLeft_toRightOf="@+id/guideline"
app:layout_constraintRight_toLeftOf="@+id/guideline2"
app:layout_constraintTop_toBottomOf="@id/stats_sensor_horizontal_line" />
<TextView
android:id="@+id/stats_sensor_heart_rate_sensor_value"
style="@style/StatsTinyValue"
android:text="@string/value_unknown"
app:layout_constraintLeft_toRightOf="@+id/guideline"
app:layout_constraintRight_toLeftOf="@+id/guideline2"
app:layout_constraintTop_toBottomOf="@+id/stats_sensor_heart_rate_label"
tools:text="90" />
<TextView
android:id="@+id/stats_sensor_heart_rate_value"
style="@style/StatsLargeValue"
android:value="@string/value_unknown"
app:layout_constraintLeft_toRightOf="@+id/guideline"
app:layout_constraintRight_toLeftOf="@+id/guideline2"
app:layout_constraintTop_toBottomOf="@+id/stats_sensor_heart_rate_sensor_value"
tools:text="90" />
<TextView
android:id="@+id/stats_sensor_heart_rate_unit"
style="@style/StatsUnit"
android:text="@string/sensor_unit_beats_per_minute"
app:layout_constraintLeft_toRightOf="@+id/guideline"
app:layout_constraintRight_toLeftOf="@+id/guideline2"
app:layout_constraintTop_toBottomOf="@+id/stats_sensor_heart_rate_value" />
<!-- Cadence -->
<androidx.constraintlayout.widget.Group
android:id="@+id/stats_sensor_cadence_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="stats_sensor_cadence_label,stats_sensor_cadence_sensor_value,stats_sensor_cadence_value,stats_sensor_cadence_unit" />
<TextView
android:id="@+id/stats_sensor_cadence_label"
style="@style/StatsLargeLabel"
android:text="@string/sensor_state_cadence"
app:layout_constrainedWidth="true"
app:layout_constraintLeft_toRightOf="@+id/guideline2"
app:layout_constraintRight_toLeftOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@id/stats_sensor_horizontal_line" />
<TextView
android:id="@+id/stats_sensor_cadence_sensor_value"
style="@style/StatsTinyValue"
android:text="@string/value_unknown"
app:layout_constraintLeft_toRightOf="@+id/guideline2"
app:layout_constraintRight_toLeftOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@+id/stats_sensor_cadence_label"
tools:text="90" />
<TextView
android:id="@+id/stats_sensor_cadence_value"
style="@style/StatsLargeValue"
android:value="@string/value_unknown"
app:layout_constraintLeft_toRightOf="@+id/guideline2"
app:layout_constraintRight_toLeftOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@+id/stats_sensor_cadence_sensor_value"
tools:text="90" />
<TextView
android:id="@+id/stats_sensor_cadence_unit"
style="@style/StatsUnit"
android:text="@string/sensor_unit_rounds_per_minute"
app:layout_constraintLeft_toRightOf="@+id/guideline2"
app:layout_constraintRight_toLeftOf="@+id/guideline3"
app:layout_constraintTop_toBottomOf="@+id/stats_sensor_cadence_value" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
+16 -11
View File
@@ -13,13 +13,14 @@ 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">
<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="@color/track_controller_background"
android:background="@drawable/track_controller_background"
android:paddingTop="8dp"
android:paddingBottom="8dp">
@@ -29,32 +30,36 @@ limitations under the License.
android:layout_height="wrap_content"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_record" />
android:contentDescription="@string/image_record"
android:src="@drawable/ic_button_record" />
<RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="6"
android:gravity="center"
android:layout_gravity="center">
android:layout_gravity="center"
android:layout_weight="6">
<TextView
android:id="@+id/track_controller_total_time"
style="@style/TextSmall"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:textColor="@android:color/white" />
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="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/track_controller_total_time"
android:layout_alignParentStart="true"
android:singleLine="true" />
android:gravity="center"
android:singleLine="true"
tools:text="@string/generic_recording" />
</RelativeLayout>
<ImageButton
@@ -64,7 +69,7 @@ limitations under the License.
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_stop"
android:src="@drawable/ic_button_stop_disabled" />
android:src="@drawable/button_stop" />
</LinearLayout>
</merge>
+1 -1
View File
@@ -63,7 +63,7 @@ limitations under the License.
android:selectAllOnFocus="true" />
</com.google.android.material.textfield.TextInputLayout>
<Spinner
<ImageView
android:id="@+id/track_edit_activity_type_icon"
android:layout_width="wrap_content"
android:layout_height="match_parent"
-468
View File
@@ -1,468 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2010 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- Activity Type -->
<string name="activity_type_airplane">fly</string>
<string name="activity_type_atv">ATV</string>
<string name="activity_type_biking">sykling</string>
<string name="activity_type_blimp">blimp (luftskip)</string>
<string name="activity_type_boat">båt</string>
<string name="activity_type_climbing">klatring</string>
<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_donkey_back_riding">eselridning</string>
<string name="activity_type_driving">kjører</string>
<string name="activity_type_driving_bus">busskjøring</string>
<string name="activity_type_driving_car">bilkjøring</string>
<string name="activity_type_ferry">ferge</string>
<string name="activity_type_frisbee">frisbee</string>
<string name="activity_type_gliding">seilflyvning</string>
<string name="activity_type_hang_gliding">hanggliding</string>
<string name="activity_type_helicopter">helikopter</string>
<string name="activity_type_hiking">fotturer</string>
<string name="activity_type_horse_back_riding">hesteridning</string>
<string name="activity_type_hot_air_balloon">varmluftsballong</string>
<string name="activity_type_ice_sailing">isseiling</string>
<string name="activity_type_inline_skating">inline-skøyter</string>
<string name="activity_type_kayaking">kajakkpadling</string>
<string name="activity_type_kite_surfing">kitesurfing</string>
<string name="activity_type_land_sailing">landseiling</string>
<string name="activity_type_mixed_type">blandet type</string>
<string name="activity_type_motor_bike">motorsykkel</string>
<string name="activity_type_motor_boating">motorbåtkjøring</string>
<string name="activity_type_mountain_biking">terrengsykling</string>
<string name="activity_type_off_trail_hiking">fotturer utenfor sti</string>
<string name="activity_type_other">andre</string>
<string name="activity_type_paddling">padling</string>
<string name="activity_type_para_gliding">paragliding</string>
<string name="activity_type_rc_airplane">Radiostyrt fly</string>
<string name="activity_type_rc_boat">Radiostyrt båt</string>
<string name="activity_type_rc_helicopter">Radiostyrt helikopter</string>
<string name="activity_type_riding">ridning</string>
<string name="activity_type_road_biking">sykling</string>
<string name="activity_type_roller_skiing">rulleski</string>
<string name="activity_type_rowing">roing</string>
<string name="activity_type_running">jogging</string>
<string name="activity_type_sailing">seiling</string>
<string name="activity_type_seaplane">sjøfly</string>
<string name="activity_type_skate_boarding">skateboarding</string>
<string name="activity_type_skating">skøyter</string>
<string name="activity_type_skiing">skisport</string>
<string name="activity_type_sky_jumping">fallskjermhopping</string>
<string name="activity_type_sled">sledekjøring</string>
<string name="activity_type_snow_boarding">snowboardkjøring</string>
<string name="activity_type_snow_shoeing">truger</string>
<string name="activity_type_speed_walking">hurtig gange</string>
<string name="activity_type_street_running">gateløp</string>
<string name="activity_type_surfing">surfing</string>
<string name="activity_type_track_cycling">banesykling</string>
<string name="activity_type_track_running">baneløp</string>
<string name="activity_type_trail_hiking">fottur på sti</string>
<string name="activity_type_trail_running">terrengløp</string>
<string name="activity_type_train">tog</string>
<string name="activity_type_ultimate_frisbee">ultimate frisbee</string>
<string name="activity_type_unknown">ukjent</string>
<string name="activity_type_wakeboarding">wakeboarding</string>
<string name="activity_type_walking">gange</string>
<string name="activity_type_water_skiing">vannski</string>
<string name="activity_type_wind_surfing">vindsurfing</string>
<!-- Description -->
<string name="description_activity_type">Aktivitetstype: %1$s</string>
<string name="description_average_moving_pace_in_minute">Gjennomsnittshastighet: %1$s min/km (%2$s min/eng. mil)</string>
<string name="description_average_moving_speed">Gjennomsnittsfart: %1$.2f km/t (%2$.1f mi/h)</string>
<string name="description_average_pace_in_minute">Gjennomsnittshastighet: %1$s min/km (%2$s min/eng. mil)</string>
<string name="description_average_speed">Gjennomsnittsfart: %1$.2f km/t (%2$.1f mi/h)</string>
<string name="description_elevation_gain">Stigning: %1$d m (%2$d ft)</string>
<string name="description_elevation_imperial">Høyde (ft)</string>
<string name="description_elevation_metric">Høyde (m)</string>
<string name="description_fastest_pace_in_minute">Høyeste hastighet: %1$s min/km (%2$s min/eng. mil)</string>
<string name="description_location_accuracy">Nøyaktighet (m)</string>
<string name="description_location_altitude">Høyde (m)</string>
<string name="description_location_bearing">Kurs (grader)</string>
<string name="description_location_latitude">Breddegrad (grader)</string>
<string name="description_location_longitude">Lengdegrad (grader)</string>
<string name="description_location_speed">Hastighet (m/s)</string>
<string name="description_max_elevation">Maksstigning: %1$d m (%2$d ft)</string>
<string name="description_max_grade">Maksgrad: %1$d %%</string>
<string name="description_max_speed">Maksfart: %1$.2f km/t (%2$.1f mi/h)</string>
<string name="description_min_elevation">Minimumsstigning: %1$d m (%2$d ft)</string>
<string name="description_min_grade">Minimumsgrad: %1$d %%</string>
<string name="description_moving_time">Flyttetid: %1$s</string>
<string name="description_pace_imperial">Hastighet (min/eng. mil)</string>
<string name="description_pace_metric">Hastighet (min/km)</string>
<string name="description_recorded_time">Spilt inn: %1$s</string>
<string name="description_speed_ms">Hastighet (m/s)</string>
<string name="description_sensor_cadence">Takt (rpm)</string>
<string name="description_sensor_heart_rate">Puls (bpm)</string>
<string name="description_sensor_power">Styrke (W)</string>
<string name="description_speed_imperial">Hastighet (km/t)</string>
<string name="description_speed_metric">Hastighet (km/t)</string>
<string name="description_time">Tid</string>
<string name="description_total_distance">Total strekning: %1$.2f km (%2$.1f mi)</string>
<string name="description_total_time">Total tid: %1$s</string>
<string name="description_track_point">Punkt</string>
<string name="description_track_segment">Segment</string>
<!-- Export -->
<string name="export_all_option">Alle som %1$s</string>
<string name="export_all_title">Eksporter til ekstern lagringsplass</string>
<string name="export_error">%1$d av %2$s ble eksportert til %3$s</string>
<string name="export_option">som %1$s til %2$s</string>
<string name="export_progress_message">Eksporterer til %1$s…</string>
<string name="export_success">%1$s ble eksportert til %2$s</string>
<string name="export_title">Eksporter til …</string>
<!-- External Storage -->
<!-- Files -->
<string name="files_few">%1$d filer</string>
<string name="files_many">%1$d filer</string>
<string name="files_one">1 fil</string>
<string name="files_other">%1$d filer</string>
<string name="files_two">2 filer</string>
<string name="files_zero">0 filer</string>
<!-- Generic -->
<string name="generic_add">Legg til</string>
<string name="generic_cancel">Avbryt</string>
<string name="generic_delete_all_confirm_title">Vil du slette alle?</string>
<string name="generic_delete_selected_confirm_title">Vil du slette de valgte elementene?</string>
<string name="generic_description">Beskrivelse</string>
<string name="generic_description_line">Beskrivelse: %1$s</string>
<string name="generic_error_title">Feil</string>
<string name="generic_name">Navn</string>
<string name="generic_name_line">Navn: %1$s</string>
<string name="generic_no">Nei</string>
<string name="generic_ok">OK</string>
<string name="generic_paused">Midlertidig stoppet</string>
<string name="generic_progress_title">Vent litt …</string>
<string name="generic_recording">Tar opp …</string>
<string name="generic_save">Lagre</string>
<string name="generic_success_title">Vellykket</string>
<string name="generic_tracks">Spor</string>
<string name="generic_yes">Ja</string>
<!-- Gps -->
<string name="gps_location_access">«Posisjonstilgang»</string>
<string name="gps_disabled">GPS er ikke aktivert. Trykk her for å gå til %1$s. Trykk på tilbake-knappen for å gå tilbake.</string>
<string name="gps_disabled_my_location">Den nåværende posisjonen kan ikke fastslås. Aktiver flere posisjonskilder i %1$s, og
pass på at himmelen er synlig. Prøv igjen senere.</string>
<string name="gps_not_found">Fant ingen GPS på enheten</string>
<string name="gps_starting">Starter GPS</string>
<string name="gps_wait_for_better_signal">Venter på bedre GPS-signal</string>
<string name="gps_wait_for_signal">Venter på GPS-signal</string>
<!-- Image -->
<string name="image_application">App</string>
<string name="image_arrow">Pil</string>
<string name="image_marker">Markør</string>
<string name="image_marker_count">Markørantall</string>
<string name="image_my_location">Min posisjon</string>
<string name="image_pause">Stans midlertidig</string>
<string name="image_photo_marker">Bildemarkør</string>
<string name="image_record">Spill inn</string>
<string name="image_stop">Stopp</string>
<string name="image_track">Spor</string>
<!-- Import -->
<string name="import_error">%1$d av %2$s ble importert fra %3$s</string>
<string name="import_no_directory">%1$s finnes ikke</string>
<string name="import_no_file">Det var ingen filer å importere. Koble enheten din til datamaskinen din, og plasser filene som skal
importeres i %1$s.</string>
<string name="import_no_file_title">Importer filer</string>
<string name="import_progress_message">Importerer fra %1$s…</string>
<string name="import_selection_option">Alle %1$s</string>
<string name="import_selection_title">Importer fra ekstern lagringsplass</string>
<string name="import_success">%1$s ble importert fra %2$s</string>
<!-- Marker -->
<string name="marker_add_error">Kan ikke sette inn markør. Har ikke GPS-signal. Prøv på nytt.</string>
<string name="marker_add_success">En markør ble satt inn</string>
<string name="marker_add_canceled">Avbrutt – ingen markør lagt til</string>
<string name="marker_delete_all_confirm_message">Alle markører for dette sporet blir slettet permanent fra enheten</string>
<string name="marker_delete_one_confirm_message">Markøren blir slettet permanent fra enheten</string>
<string name="marker_delete_one_confirm_title">Vil du slette markøren?</string>
<string name="marker_delete_multiple_confirm_message">De valgte markørene blir slettet permanent fra enheten</string>
<string name="marker_label_end">%1$s (slutt)</string>
<string name="marker_label_start">%1$s (start)</string>
<string name="marker_name_format">Markør %1$d</string>
<string name="marker_split_name_format">Del %1$d</string>
<string name="marker_title">%1$d av %2$d</string>
<!-- Marker Detail -->
<string name="marker_detail_statistics_title">Siden forrige delmarkør:</string>
<!-- Marker Edit -->
<string name="marker_edit_marker_type">Markørtype (f.eks. foss)</string>
<string name="marker_edit_marker_type_hint">Markørtype</string>
<!-- Marker List -->
<string name="marker_list_empty_message">Du har ingen markører</string>
<!-- Menu -->
<string name="menu_aggregated_statistics">Samlet statistikk</string>
<string name="menu_delete">Slett</string>
<string name="menu_edit">Rediger</string>
<string name="menu_export_all">Eksporter alle</string>
<string name="menu_help">Hjelp</string>
<string name="menu_import_all">Importer alle</string>
<string name="menu_insert_marker">Sett inn markør</string>
<string name="menu_insert_photo">Sett inn bilde</string>
<string name="menu_markers">Markører</string>
<string name="menu_search">Søk</string>
<string name="menu_select_all">Velg alle</string>
<string name="menu_sensor_state">Sensortilstand</string>
<string name="menu_settings">Innstillinger</string>
<string name="menu_share">Del</string>
<string name="menu_show_on_map">Vis på kart</string>
<string name="menu_split_frequency">Delefrekvens</string>
<string name="menu_start_gps">Start GPS</string>
<string name="menu_stop_gps">Stopp GPS</string>
<string name="menu_view_photo">Se bildet</string>
<string name="menu_voice_frequency">Talefrekvens</string>
<!-- OpenTracks -->
<string name="app_name">OpenTracks</string>
<string name="recording_service">Opptakstjeneste</string>
<!-- Search -->
<string name="search_global_description">Spor og markører</string>
<string name="search_list_empty_message">Ingen spor eller markører samsvarte med søket ditt</string>
<string name="search_list_marker_track_location">I «%1$s»-stien.</string>
<!-- Sensor State -->
<string name="sensor_state_battery">Batterinivå</string>
<string name="sensor_state_cadence">Kadens</string>
<string name="sensor_state_cadence_value">%1$d rpm</string>
<string name="sensor_state_disconnected">Frakoblet</string>
<string name="sensor_state_heart_rate">Puls</string>
<string name="sensor_state_heart_rate_value">%1$d bpm</string>
<string name="sensor_state_power">Strøm</string>
<string name="sensor_state_power_value">%1$d W</string>
<string name="settings_not_while_recording">Ikke tilgjengelig mens du tar opp en sti.</string>
<string name="settings_show">Vis</string>
<!-- Settings Advanced -->
<string name="settings_advanced">Avansert</string>
<!-- Settings Chart -->
<string name="settings_chart_by_distance">Etter distanse</string>
<string name="settings_chart_by_time">Etter tid</string>
<string name="settings_chart_x_axis">X-akse</string>
<string name="settings_recording">Opptak</string>
<string name="settings_recording_auto_resume_track_timeout_always_summary">Fortsett alltid opptak etter omstart</string>
<string name="settings_recording_auto_resume_track_timeout_never_summary">Fortsett aldri opptak etter omstart</string>
<string name="settings_recording_auto_resume_track_timeout_summary">Fortsett opptak etter omstart hvis den sist innspilte posisjonen var innen de siste %1$s</string>
<string name="settings_recording_auto_resume_track_timeout_title">Auto-gjenoppta</string>
<string name="settings_recording_default_activity_title">Standard aktivitetstype</string>
<string name="settings_recording_location_frequency_summary">%1$s mellom innspilte posisjoner</string>
<string name="settings_recording_max_recording_distance_summary">Det blir opprettet et nytt segment hvis avstanden mellom to posisjoner er større enn %1$s</string>
<string name="settings_recording_max_recording_distance_title">Maks avstand</string>
<string name="settings_recording_min_recording_distance_title">Spiller inn avstandsintervall</string>
<string name="settings_recording_min_recording_interval_title">Spiller inn tidsintervall</string>
<string name="settings_recording_min_required_accuracy_summary">Posisjonen ignoreres hvis nøyaktigheten er under %1$s</string>
<string name="settings_recording_min_required_accuracy_title">GPS-nøyaktighet</string>
<string name="settings_recording_track_name_date_iso_8601_option">Dato (ISO 8601)</string>
<string name="settings_recording_track_name_date_local_option">Dato (lokal)</string>
<string name="settings_recording_track_name_number_option">Tall</string>
<string name="settings_recording_track_name_title">Standardnavn på sti</string>
<!-- Settings Reset -->
<string name="settings_reset">Tilbakestill</string>
<string name="settings_reset_confirm_message">Alle innstillinger går tilbake til standardverdiene.</string>
<string name="settings_reset_confirm_title">Vil du tilbakestille nå?</string>
<string name="settings_reset_done">Alle innstillinger er tilbakestilt til standardverdiene</string>
<!-- Settings Sensor -->
<string name="settings_sensor">Sensor</string>
<string name="settings_sensor_bluetooth">Bluetooth</string>
<string name="settings_sensor_bluetooth_sensor">Bluetooth-sensor</string>
<!-- Settings Stats -->
<string name="settings_stats_coordinate">Bredde- og lengdegrad</string>
<string name="settings_stats_rate_title">Foretrukket måling</string>
<string name="settings_stats_units_imperial">Imperial (mi, ft)</string>
<string name="settings_stats_units_metric">Metrisk (km, m)</string>
<string name="settings_stats_units_title">Foretrukne enheter</string>
<!-- Share Track -->
<string name="share_track_share_file">Del sporfilen</string>
<string name="share_track_share_file_body">Jeg tror du kan være interessert i dette sporet.\n\nFor å se på dette sporet bruker du Google Earth eller annen
kartprogramvare.\n\n%1$s</string>
<string name="share_track_subject">Jeg vil dele en sti med deg</string>
<!-- Stats -->
<string name="stats_average_moving_pace">Gjennomsnittlig forflytningstempo</string>
<string name="stats_average_moving_speed">Gjennomsnittlig forflytningshastighet</string>
<string name="stats_average_pace">Gjennomsnittlig tempo</string>
<string name="stats_average_speed">Gjennomsnittlig hastighet</string>
<string name="stats_elevation">Høyde</string>
<string name="stats_fastest_pace">Raskeste tempo</string>
<string name="stats_gain">Økning</string>
<string name="stats_grade">Stigning</string>
<string name="stats_latitude">Breddegrad</string>
<string name="stats_longitude">Lengdegrad</string>
<string name="stats_max">Maks</string>
<string name="stats_max_speed">Maks. hastighet</string>
<string name="stats_min">Min</string>
<string name="stats_moving_time">Forflyttingstid</string>
<string name="stats_pace">Tempo</string>
<string name="stats_speed">Hastighet</string>
<string name="stats_distance">Avstand</string>
<string name="stats_total_time">Total tid</string>
<!-- Track -->
<string name="track_name_format">Sti %1$d</string>
<string name="track_record_notification">Tar opp stien …</string>
<string name="track_markers">Markører for %1$s</string>
<!-- Track Delete -->
<string name="track_delete_one_confirm_message">Sporet og markørene slettes permanent fra enheten.</string>
<string name="track_delete_one_confirm_title">Vil du slette sporet?</string>
<string name="track_delete_multiple_confirm_message">De valgte sporene og markørene slettes permanent fra enheten.</string>
<string name="track_delete_progress_message">Sletter …</string>
<!-- Track Detail -->
<string name="track_detail_chart_tab">Diagram</string>
<string name="track_detail_stats_tab">Statistikk</string>
<!-- Track Edit -->
<string name="track_edit_activity_type">Aktivitetstype (f.eks. turgåing)</string>
<string name="track_edit_activity_type_hint">Aktivitetstype</string>
<string name="track_edit_new_track_title">Nytt spor</string>
<!-- Track List -->
<string name="track_list_empty_message">Begynn å spille inn ditt neste eventyr her</string>
<!-- Track Widget -->
<!-- Tracks -->
<string name="tracks_few">%1$d stier</string>
<string name="tracks_many">%1$d stier</string>
<string name="tracks_one">1 sti</string>
<string name="tracks_other">%1$d stier</string>
<string name="tracks_two">2 stier</string>
<string name="tracks_zero">0 stier</string>
<!-- Unit -->
<string name="unit_feet">fot</string>
<string name="unit_kilometer">km</string>
<string name="unit_kilometer_per_hour">km/t</string>
<string name="unit_meter">m</string>
<string name="unit_mile">eng. mil</string>
<string name="unit_mile_per_hour">eng. mil per time</string>
<string name="unit_minute_per_kilometer">min/km</string>
<string name="unit_minute_per_mile">min per eng. mil</string>
<!-- Value -->
<string name="value_adapt_accuracy">Tilpass for nøyaktighet</string>
<string name="value_adapt_battery_life">Tilpass for batterilevetid</string>
<string name="value_always">Alltid</string>
<string name="value_float_feet">%1$.2f fot</string>
<string name="value_float_kilometer">%1$.2f km</string>
<string name="value_float_meter">%1$.2f m</string>
<string name="value_float_mile">%1$.2f eng. mil</string>
<string name="value_float_mile_poor_gps">%1$.1f eng. mil (dårlig GPS)</string>
<string name="value_integer_feet">%1$d fot</string>
<string name="value_integer_feet_excellent_gps">%1$d fot (utmerket GPS)</string>
<string name="value_integer_feet_recommended">%1$d fot (anbefales)</string>
<string name="value_integer_kilometer">%1$d km</string>
<string name="value_integer_kilometer_hour">%1$d km/t</string>
<string name="value_integer_meter">%1$d m</string>
<string name="value_integer_meter_excellent_gps">%1$d m (utmerket GPS)</string>
<string name="value_integer_meter_poor_gps">%1$d m (dårlig GPS)</string>
<string name="value_integer_meter_recommended">%1$d m (anbefales)</string>
<string name="value_integer_mile">%1$d eng. mil</string>
<string name="value_integer_mile_hour">%1$d mi/h</string>
<string name="value_integer_minute">%1$d min</string>
<string name="value_integer_percent">%1$d %%</string>
<string name="value_integer_second">%1$d s</string>
<string name="value_none">Ingen</string>
<string name="value_off">Av</string>
<string name="value_smallest">Minst</string>
<string name="value_smallest_recommended">Minste (anbefales)</string>
<!-- Voice -->
<string name="voice_hours_few">%1$d timer</string>
<string name="voice_hours_many">%1$d timer</string>
<string name="voice_hours_one">Én time</string>
<string name="voice_hours_other">%1$d timer</string>
<string name="voice_hours_two">To timer</string>
<string name="voice_hours_zero">Null timer</string>
<string name="voice_minutes_few">%1$d minutter</string>
<string name="voice_minutes_many">%1$d minutter</string>
<string name="voice_minutes_one">Ett minutt</string>
<string name="voice_minutes_other">%1$d minutter</string>
<string name="voice_minutes_two">To minutter</string>
<string name="voice_minutes_zero">Null minutter</string>
<string name="voice_pace_per_kilometer">%1$s per kilometer</string>
<string name="voice_pace_per_mile">%1$s per engelske mil</string>
<string name="voice_seconds_few">%1$d sekunder</string>
<string name="voice_seconds_many">%1$d sekunder</string>
<string name="voice_seconds_one">Ett sekund</string>
<string name="voice_seconds_other">%1$d sekunder</string>
<string name="voice_seconds_two">To sekunder</string>
<string name="voice_seconds_zero">Null sekunder</string>
<string name="voice_speed_kilometers_per_hour_few">%1$.1f kilometer i timen</string>
<string name="voice_speed_kilometers_per_hour_many">%1$.1f kilometer i timen</string>
<string name="voice_speed_kilometers_per_hour_one">Én kilometer i timen</string>
<string name="voice_speed_kilometers_per_hour_other">%1$.1f kilometer i timen</string>
<string name="voice_speed_kilometers_per_hour_two">To kilometer i timen</string>
<string name="voice_speed_kilometers_per_hour_zero">Null kilometer i timen</string>
<string name="voice_speed_miles_per_hour_few">%1$.1f engelske mil i timen</string>
<string name="voice_speed_miles_per_hour_many">%1$.1f engelske mil i timen</string>
<string name="voice_speed_miles_per_hour_one">Én engelsk mil i timen</string>
<string name="voice_speed_miles_per_hour_other">%1$.1f engelske mil i timen</string>
<string name="voice_speed_miles_per_hour_two">To engelske mil i timen</string>
<string name="voice_speed_miles_per_hour_zero">Null engelske mil i timen</string>
<string name="voice_template">Spor %1$s om %2$s ved %3$s</string>
<string name="voice_total_distance_kilometers_few">total avstand er %1$.2f kilometer</string>
<string name="voice_total_distance_kilometers_many">total avstand er %1$.2f kilometer</string>
<string name="voice_total_distance_kilometers_one">total avstand er én kilometer</string>
<string name="voice_total_distance_kilometers_other">total avstand er %1$.2f kilometer</string>
<string name="voice_total_distance_kilometers_two">total avstand er to kilometer</string>
<string name="voice_total_distance_kilometers_zero">total avstand er null kilometer</string>
<string name="voice_total_distance_miles_few">total avstand er %1$.2f engelske mil</string>
<string name="voice_total_distance_miles_many">total avstand er %1$.2f engelske mil</string>
<string name="voice_total_distance_miles_one">total avstand er én engelsk mil</string>
<string name="voice_total_distance_miles_other">total avstand er %1$.2f engelske mil</string>
<string name="voice_total_distance_miles_two">total avstand er to engelske mil</string>
<string name="voice_total_distance_miles_zero">total avstand er null engelske mil</string>
<string name="voice_total_distance_zero">Total avstand i Spor er null.</string>
<!-- Waypoint Type -->
<string name="waypoint_type_atm">Minibank</string>
<string name="waypoint_type_bank">bank</string>
<string name="waypoint_type_bar">bar</string>
<string name="waypoint_type_barbecue">grillmat</string>
<string name="waypoint_type_building">bygning</string>
<string name="waypoint_type_bus_stop">busstopp</string>
<string name="waypoint_type_cafe">kafé</string>
<string name="waypoint_type_campfire">leirbål</string>
<string name="waypoint_type_campground">campingplass</string>
<string name="waypoint_type_church">kirke</string>
<string name="waypoint_type_coffee_house">kaffebar</string>
<string name="waypoint_type_convenience_store">nærbutikk</string>
<string name="waypoint_type_drinking_water">drikkevann</string>
<string name="waypoint_type_falling_rocks">fallende steiner</string>
<string name="waypoint_type_ferry">ferge</string>
<string name="waypoint_type_fire_place">ildsted</string>
<string name="waypoint_type_fire_station">brannstasjon</string>
<string name="waypoint_type_fishing">fiske</string>
<string name="waypoint_type_gas_station">bensinstasjon</string>
<string name="waypoint_type_golf_course">golfbane</string>
<string name="waypoint_type_grocery_store">dagligvarebutikk</string>
<string name="waypoint_type_hair_dresser">frisør</string>
<string name="waypoint_type_harbor">havn</string>
<string name="waypoint_type_hiking_path">tursti</string>
<string name="waypoint_type_hotel">hotell</string>
<string name="waypoint_type_house">hus</string>
<string name="waypoint_type_information">informasjon</string>
<string name="waypoint_type_intersection">veikryss</string>
<string name="waypoint_type_mail_box">postkasse</string>
<string name="waypoint_type_market">marked</string>
<string name="waypoint_type_meeting_point">møtested</string>
<string name="waypoint_type_opera">opera</string>
<string name="waypoint_type_parking">parkering</string>
<string name="waypoint_type_police_station">politistasjon</string>
<string name="waypoint_type_post_office">postkontor</string>
<string name="waypoint_type_pothole">jettegryte</string>
<string name="waypoint_type_railway_crossing">jernbaneovergang</string>
<string name="waypoint_type_restaurant">restaurant</string>
<string name="waypoint_type_shopping">shopping</string>
<string name="waypoint_type_sight">attraksjon</string>
<string name="waypoint_type_store">butikk</string>
<string name="waypoint_type_swimming_pool">svømmebasseng</string>
<string name="waypoint_type_telephone">telefon</string>
<string name="waypoint_type_theater">teater</string>
<string name="waypoint_type_toilet">toalett</string>
<string name="waypoint_type_tourist_information">turistinformasjon</string>
<string name="waypoint_type_tree">tre</string>
<string name="waypoint_type_tunnel">tunnel</string>
<string name="waypoint_type_waterfall">foss</string>
<string name="waypoint_type_water_fountain">vannfontene</string>
<string name="waypoint_type_waypoint">veipunkt</string>
</resources>
+4 -3
View File
@@ -22,10 +22,10 @@ limitations under the License.
<color name="chart_heart_rate_fill">#40E88A70</color> <!-- transparent red -->
<color name="chart_pace_border">#FF0066FF</color> <!-- blue -->
<color name="chart_pace_fill">#40C8E0FF</color> <!-- transparent blue -->
<color name="chart_power_border">#FF401465</color> <!-- purple -->
<color name="chart_power_fill">#40401465</color> <!-- transparent purple -->
<color name="chart_speed_border">#FF0066FF</color> <!-- blue -->
<color name="chart_speed_fill">#40C8E0FF</color> <!-- transparent blue -->
<color name="chart_power_border">#FF7A26C0</color> <!-- purple -->
<color name="chart_power_fill">#407A26C0</color> <!-- transparent purple -->
<color name="colorAccent">@android:color/holo_orange_dark</color>
<color name="colorAccentTransparent">#55FF8800</color>
@@ -35,7 +35,8 @@ limitations under the License.
<color name="text_gradient_center">#B2000000</color> <!-- black 70% opacity -->
<color name="text_gradient_end">#00000000</color> <!-- black 0% opacity -->
<color name="text_gradient_start">#FF000000</color> <!-- black 100% opacity -->
<color name="track_controller_background">#FF0E0E0E</color> <!-- dark gray -->
<color name="track_controller_background_normal">#FF0E0E0E</color> <!-- dark gray -->
<color name="track_controller_background_activated">#3F0000</color>
<color name="splashscreen_background">#272727</color>
</resources>
+1
View File
@@ -26,6 +26,7 @@ limitations under the License.
<string name="sensor_unit_beats_per_minute" translatable="false">BPM</string>
<string name="sensor_unit_rounds_per_minute" translatable="false">RPM</string>
<string name="sensor_unit_power" translatable="false">W</string>
<string name="value_unknown" translatable="false">-</string>
+1
View File
@@ -21,6 +21,7 @@
<string name="settings_sensor_bluetooth_heart_rate_key" translatable="false">bluetoothSensor</string>
<string name="settings_sensor_bluetooth_cycling_cadence_key" translatable="false">bluetoothCyclingCadenceSensor</string>
<string name="settings_sensor_bluetooth_cycling_speed_key" translatable="false">bluetoothCyclingSpeedSensor</string>
<string name="settings_sensor_bluetooth_cycling_power_key" translatable="false">bluetoothCyclingPowerSensor</string>
<string name="sensor_type_value_none" translatable="false">NONE</string>
<string name="settings_sensor_bluetooth_cycling_speed_wheel_circumference_key" translatable="false">bluetoothCyclingSpeedWheelCircumference</string>
+4 -2
View File
@@ -212,6 +212,7 @@ limitations under the License.
<string name="export_with_photos">with photos</string>
<string name="export_without_photos">without photos</string>
<string name="export_error">Exported %1$d of %2$s to %3$s</string>
<string name="export_error_post_workout">Post-workout export failed, please check the export directory.</string>
<string name="export_option">as %1$s to %2$s</string>
<string name="export_progress_message">Exporting to %1$s&#8230;</string>
<string name="export_success">Exported %1$s to %2$s</string>
@@ -285,6 +286,7 @@ limitations under the License.
<string name="image_stop">Stop</string>
<string name="image_track">Track</string>
<!-- Import -->
<string name="import_thread_interrupted">Thread interrupted</string>
<string name="import_unsupported_format">Unsupported file format</string>
<string name="import_parser_error">Parser error: %1$s</string>
<string name="import_unable_to_import_file">Unable to import file: %1$s</string>
@@ -631,8 +633,8 @@ limitations under the License.
<string name="share_image_subject">I would like to share an image with you</string>
<string name="share_image_body">I think you might be interested in this image.</string>
<string name="hold_to_stop">Hold to stop recording</string>
<string name="hold_to_pause">Hold to pause recording</string>
<string name="hold_to_stop">Hold to stop</string>
<string name="hold_to_pause">Hold to pause</string>
<string name="toast_message_instant_track_was_exported">Last track exported</string>
<string name="instant_export_enabled_summary">Export track to storage after recording is finished</string>
+1 -3
View File
@@ -2,6 +2,4 @@
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/menu_search"
android:includeInGlobalSearch="false"
android:label="@string/app_name"
android:queryAfterZeroResults="true"
android:searchSettingsDescription="@string/search_global_description" />
android:label="@string/app_name" />
+4
View File
@@ -131,6 +131,10 @@ limitations under the License.
android:defaultValue="@string/sensor_type_value_none"
android:key="@string/settings_sensor_bluetooth_cycling_cadence_key"
android:title="@string/sensor_state_cadence" />
<de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingPowerPreference
android:defaultValue="@string/sensor_type_value_none"
android:key="@string/settings_sensor_bluetooth_cycling_power_key"
android:title="@string/sensor_state_power" />
<de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference
android:defaultValue="@string/sensor_type_value_none"
android:key="@string/settings_sensor_bluetooth_cycling_speed_key"