mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-10-25 10:44:44 +01:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 397e056f6f | |||
| eb79f2277a | |||
| ae8833d21b | |||
| e8017a4e9d | |||
| cea180f871 | |||
| be1b6ece9d | |||
| 0592863d3c | |||
| 18bc100d5a | |||
| 4476a15558 | |||
| 215c28be85 | |||
| a429fbe4df | |||
| 6b470c7fdc | |||
| 6b690f38ad | |||
| f138826d60 | |||
| 14ac29095d | |||
| a3f4de7d18 | |||
| 6392e88dc7 | |||
| 1e913d6bb5 | |||
| ced8454f59 | |||
| 974a5afeb1 | |||
| 9c9bdde6b9 | |||
| ca2bc6426c | |||
| 81097aac40 | |||
| a74b5fc743 | |||
| e996ad0a0c | |||
| 0f726958e0 | |||
| 3a84520819 | |||
| 8328069b9d | |||
| b2df79caf2 | |||
| 6bcb8db40c | |||
| 974baaf611 | |||
| fd77e77bb9 | |||
| 1e8beff015 | |||
| 96bc347374 | |||
| 8f144d4a97 | |||
| 3c3641376d | |||
| 35f4427aec | |||
| 5a0cb10838 | |||
| bf92851c97 | |||
| 55c45b2050 | |||
| d01715444f | |||
| 634a7a4321 | |||
| e5911a79ab | |||
| a78b3eb66d | |||
| 28890abd47 | |||
| 0a466d240b | |||
| 9ef312fc93 | |||
| 56aab12110 | |||
| c394448273 | |||
| 95671f4927 | |||
| 348341204e | |||
| 82f5c127ee | |||
| dd490f528e | |||
| 27731319f0 | |||
| 86bfe6d0ce | |||
| ea8bbb0b36 | |||
| c02fdd3bba |
-10
@@ -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
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
_OpenTracks_ is a sport tracking application that completely respects your privacy.
|
||||
|
||||
[](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
@@ -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/).
|
||||
@@ -1,21 +1,44 @@
|
||||
# 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)
|
||||
* Mi Band 3, Amazfit Band 5, Amazfit Bip. These devices are manufactured by Huami, and other Huami devices will very likely work. Theoretically, this sensor must be enabled, but testing reveals that it works even without enabling. For details on how to enable see [Gadgetbridge wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Huami-Heartrate-measurement#bluetooth-heart-rate-sensor)
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ cp "$CHANGELOG_DIR/next_release.txt" "$CHANGELOG_FILE"
|
||||
sed -i "s/(versionName)/$VERSIONNAME/" "$CHANGELOG_FILE"
|
||||
|
||||
echo -e "\nGit commit since last release\n------------"
|
||||
git log $(git describe --tags --abbrev=0)..HEAD --no-merges --oneline
|
||||
git log "$(git describe --tags --abbrev=0)..HEAD" --no-merges --oneline
|
||||
|
||||
echo -e "\nPlease edit the changelog for the new release"
|
||||
gedit -w "$CHANGELOG_FILE"
|
||||
|
||||
+5
-5
@@ -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 3761
|
||||
versionName "v3.14.0"
|
||||
|
||||
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'
|
||||
@@ -98,5 +98,5 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'org.mockito:mockito-android:3.6.0'
|
||||
androidTestImplementation 'org.mockito:mockito-android:3.6.28'
|
||||
}
|
||||
|
||||
+9
-10
@@ -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
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
v3.14.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Properly support for right-to-left
|
||||
- Import: faster cleanup after import failure
|
||||
- Text in charts scales according to Android settings
|
||||
- Heart rate: support for Huami devices (requires Gadgetbridge)
|
||||
|
||||
Developer:
|
||||
- Import is now handled in-memory
|
||||
@@ -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
|
||||
|
||||
@@ -48,7 +48,9 @@ public class ChartValueSeriesTest {
|
||||
R.string.description_elevation_metric,
|
||||
R.string.description_elevation_imperial,
|
||||
R.color.chart_elevation_fill,
|
||||
R.color.chart_elevation_border) {
|
||||
R.color.chart_elevation_border,
|
||||
15,
|
||||
18) {
|
||||
@Override
|
||||
double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getElevation();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -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() {
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package de.dennisguse.opentracks.io.file.importer;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
/**
|
||||
* Imports a legacy kml file with a statistics marker at the beginning
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class LegacyKmlImportTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
|
||||
private Track.Id importTrackId;
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (importTrackId != null) {
|
||||
contentProviderUtils.deleteTrack(context, importTrackId);
|
||||
}
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kml_legacy_track_import() {
|
||||
// given
|
||||
KmlFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
|
||||
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_track);
|
||||
|
||||
// when
|
||||
// 1. import
|
||||
importTrackId = trackImporter.importFile(inputStream);
|
||||
|
||||
// then
|
||||
// 1. track
|
||||
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
|
||||
assertNotNull(importedTrack);
|
||||
assertEquals("unknown", importedTrack.getCategory());
|
||||
assertEquals("Test Track", importedTrack.getDescription());
|
||||
assertEquals("2020-11-28 18:06", importedTrack.getName());
|
||||
assertEquals("UNKNOWN", importedTrack.getIcon());
|
||||
|
||||
// 2. markers
|
||||
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
|
||||
|
||||
// 3. trackpoints
|
||||
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
|
||||
assertEquals(8, importedTrackPoints.size()); // 6 trackpoints + pause and resume
|
||||
|
||||
// first 3 trackpoints
|
||||
assertTrackpoint(importedTrackPoints.get(0), "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
|
||||
assertTrackpoint(importedTrackPoints.get(1), "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
|
||||
assertTrackpoint(importedTrackPoints.get(2), "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
|
||||
|
||||
// created pause trackpoint with time of previous valid trackpoint
|
||||
assertTrackpoint(importedTrackPoints.get(3), "2020-11-28T17:06:47.888Z", 0.0, 100, 0.0);
|
||||
// created resume trackpoint with time of next valid trackpoint
|
||||
assertTrackpoint(importedTrackPoints.get(4), "2020-11-28T17:06:55.861Z", 0.0, 200, 0.0);
|
||||
|
||||
// last 3 trackpoints
|
||||
assertTrackpoint(importedTrackPoints.get(5), "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
|
||||
assertTrackpoint(importedTrackPoints.get(6), "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
|
||||
assertTrackpoint(importedTrackPoints.get(7), "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
|
||||
}
|
||||
|
||||
private void assertTrackpoint(final TrackPoint trackPoint, final String when, final double longitude, final double latitude, final double altitude) {
|
||||
assertEquals(StringUtils.parseTime(when), trackPoint.getTime());
|
||||
assertEquals(latitude, trackPoint.getLatitude(), 0.001d);
|
||||
assertEquals(longitude, trackPoint.getLongitude(), 0.001d);
|
||||
assertEquals(altitude, trackPoint.getAltitude(), 0.001d);
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@ import org.junit.Test;
|
||||
*/
|
||||
public class ElevationSumManagerTest {
|
||||
|
||||
private ElevationSumManager elevationSumManager = new ElevationSumManager();
|
||||
private final ElevationSumManager elevationSumManager = new ElevationSumManager();
|
||||
|
||||
private static void addSensorValue(ElevationSumManager elevationSumManager, float[] values) {
|
||||
for (float f : values) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -226,4 +226,13 @@ public class StringUtilsTest {
|
||||
assertEquals("min/km", StringUtils.getSpeedParts(context, 0, true, false).second);
|
||||
assertEquals("min/mi", StringUtils.getSpeedParts(context, 0, false, false).second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatSpeed() {
|
||||
assertEquals("4:59 min/km", StringUtils.formatSpeed(context, 3.34, true, false));
|
||||
assertEquals("8:02 min/mi", StringUtils.formatSpeed(context, 3.34, false, false));
|
||||
|
||||
assertEquals("12.02 km/h", StringUtils.formatSpeed(context, 3.34, true, true));
|
||||
assertEquals("7.47 mph", StringUtils.formatSpeed(context, 3.34, false, true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2"
|
||||
xmlns:gx="http://www.google.com/kml/ext/2.2"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1">
|
||||
<Document>
|
||||
<open>1</open>
|
||||
<visibility>1</visibility>
|
||||
<name><![CDATA[2020-11-28 18:06]]></name>
|
||||
<atom:generator><![CDATA[OpenTracks (Debug)]]></atom:generator>
|
||||
<Style id="track">
|
||||
<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>
|
||||
<IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://earth.google.com/images/kml-icons/track-directional/track-0.png</href></Icon>
|
||||
</IconStyle>
|
||||
</Style>
|
||||
<Style id="start"><IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/grn-circle.png</href></Icon>
|
||||
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
|
||||
</IconStyle></Style>
|
||||
<Style id="end"><IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/red-circle.png</href></Icon>
|
||||
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
|
||||
</IconStyle></Style>
|
||||
<Style id="waypoint"><IconStyle>
|
||||
<scale>1.3</scale>
|
||||
<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png</href></Icon>
|
||||
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
|
||||
</IconStyle></Style>
|
||||
<Schema id="schema">
|
||||
<gx:SimpleArrayField name="speed" type="float">
|
||||
<displayName><![CDATA[Speed (m/s)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="power" type="float">
|
||||
<displayName><![CDATA[Power (W)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="cadence" type="float">
|
||||
<displayName><![CDATA[Cadence (rpm)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="heart_rate" type="float">
|
||||
<displayName><![CDATA[Heart rate (bpm)]]></displayName>
|
||||
</gx:SimpleArrayField>
|
||||
</Schema>
|
||||
<Folder>
|
||||
<name><![CDATA[Sun, 28 Nov 2020 18:06 Markers]]></name>
|
||||
<open>1</open>
|
||||
<Placemark>
|
||||
<name><![CDATA[Split 0]]></name>
|
||||
<description><![CDATA[Total distance: 0.00 km (0.0 mi)
|
||||
Total time: 00:00
|
||||
Moving time: 00:00
|
||||
Average speed: 0.00 km/h (0.0 mi/h)
|
||||
Average moving speed: 0.00 km/h (0.0 mi/h)
|
||||
Max speed: 0.00 km/h (0.0 mi/h)
|
||||
Average pace: 0:00 min/km (0:00 min/mi)
|
||||
Average moving pace: 0:00 min/km (0:00 min/mi)
|
||||
Fastest pace: 0:00 min/km (0:00 min/mi)
|
||||
Max elevation: -9223372036854775808 m (-9223372036854775808 ft)
|
||||
Min elevation: 9223372036854775807 m (9223372036854775807 ft)
|
||||
Elevation gain: 0 m (0 ft)
|
||||
Max grade: 0 %
|
||||
Min grade: 0 %
|
||||
Recorded: 6/23 11:29
|
||||
]]></description>
|
||||
<TimeStamp><when>1970-01-01T00:00:00Z</when></TimeStamp>
|
||||
<styleUrl>#waypoint</styleUrl>
|
||||
<Point>
|
||||
<coordinates>180.0,100.0</coordinates>
|
||||
</Point>
|
||||
</Placemark>
|
||||
</Folder>
|
||||
<Placemark>
|
||||
<name><![CDATA[2020-11-28 18:06 (Start)]]></name>
|
||||
<description><![CDATA[]]></description>
|
||||
<TimeStamp><when>2020-11-28T17:06:22.401Z</when></TimeStamp>
|
||||
<styleUrl>#start</styleUrl>
|
||||
<Point>
|
||||
<coordinates>1.234156,12.340097,469.286376953125</coordinates>
|
||||
</Point>
|
||||
</Placemark>
|
||||
<Placemark>
|
||||
<name><![CDATA[2020-11-28 18:06]]></name>
|
||||
<description><![CDATA[Test Track]]></description>
|
||||
<icon><![CDATA[UNKNOWN]]></icon>
|
||||
<opentracks:trackid>2f170875-55d7-445d-b2b8-22a573c81dbc</opentracks:trackid>
|
||||
<styleUrl>#track</styleUrl>
|
||||
<ExtendedData>
|
||||
<Data name="type"><value><![CDATA[unknown]]></value></Data>
|
||||
</ExtendedData>
|
||||
<gx:MultiTrack>
|
||||
<altitudeMode>absolute</altitudeMode>
|
||||
<gx:interpolate>1</gx:interpolate>
|
||||
<gx:Track>
|
||||
<when>2020-11-28T17:06:22.401Z</when>
|
||||
<gx:coord>1.234156 12.340097 469.286376953125</gx:coord>
|
||||
<when>2020-11-28T17:06:25.448Z</when>
|
||||
<gx:coord>1.23415 12.340036 439.1626281738281</gx:coord>
|
||||
<when>2020-11-28T17:06:47.888Z</when>
|
||||
<gx:coord>1.23405 12.340057 421.8070983886719</gx:coord>
|
||||
<ExtendedData>
|
||||
<SchemaData schemaUrl="#schema">
|
||||
<gx:SimpleArrayData name="speed">
|
||||
<gx:value>0.5391469</gx:value>
|
||||
<gx:value>0.15777911</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
</gx:SimpleArrayData>
|
||||
<gx:SimpleArrayData name="elevation_gain">
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
</gx:SimpleArrayData>
|
||||
</SchemaData>
|
||||
</ExtendedData>
|
||||
</gx:Track>
|
||||
<gx:Track>
|
||||
<when>2020-11-28T17:06:55.861Z</when>
|
||||
<gx:coord>1.23405 12.340057 419.93902587890625</gx:coord>
|
||||
<when>2020-11-28T17:06:56.905Z</when>
|
||||
<gx:coord>1.23405 12.340057 419.9036560058594</gx:coord>
|
||||
<when>2020-11-28T17:07:20.870Z</when>
|
||||
<gx:coord>1.234046 12.340082 417.99432373046875</gx:coord>
|
||||
<ExtendedData>
|
||||
<SchemaData schemaUrl="#schema">
|
||||
<gx:SimpleArrayData name="speed">
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
</gx:SimpleArrayData>
|
||||
<gx:SimpleArrayData name="elevation_gain">
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
<gx:value>0.0</gx:value>
|
||||
</gx:SimpleArrayData>
|
||||
</SchemaData>
|
||||
</ExtendedData>
|
||||
</gx:Track>
|
||||
</gx:MultiTrack>
|
||||
</Placemark>
|
||||
<Placemark>
|
||||
<name><![CDATA[2020-11-28 18:06 (End)]]></name>
|
||||
<description><![CDATA[OpenTracks (Debug)
|
||||
|
||||
Name: 2020-11-28 18:06
|
||||
Activity type: unknown
|
||||
Description: -
|
||||
Total distance: 0.01 km (0.0 mi)
|
||||
Total time: 01:04
|
||||
Moving time: 00:00
|
||||
Average speed: 0.49 km/h (0.3 mi/h)
|
||||
Average moving speed: 0.00 km/h (0.0 mi/h)
|
||||
Max speed: 0.00 km/h (0.0 mi/h)
|
||||
Average pace: 122:30 min/km (197:08 min/mi)
|
||||
Average moving pace: 0:00 min/km (0:00 min/mi)
|
||||
Fastest pace: 0:00 min/km (0:00 min/mi)
|
||||
Max elevation: 469 m (1540 ft)
|
||||
Min elevation: 419 m (1376 ft)
|
||||
Elevation gain: 0 m (0 ft)
|
||||
Recorded: 11/28 18:06
|
||||
]]></description>
|
||||
<TimeStamp><when>2020-11-28T17:07:20.870Z</when></TimeStamp>
|
||||
<styleUrl>#end</styleUrl>
|
||||
<Point>
|
||||
<coordinates>1.234046,12.340082,417.99432373046875</coordinates>
|
||||
</Point>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
@@ -50,10 +50,11 @@ limitations under the License.
|
||||
<application
|
||||
android:name=".Startup"
|
||||
android:allowBackup="false"
|
||||
android:hasFragileUserData="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/ThemeCustom">
|
||||
|
||||
<activity android:name=".AboutActivity" />
|
||||
@@ -71,14 +72,14 @@ limitations under the License.
|
||||
<activity android:name=".io.file.importer.ImportActivity">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="application/gpx+xml"/>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/gpx+xml" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="application/gpx"/>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/gpx" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter
|
||||
@@ -86,13 +87,15 @@ limitations under the License.
|
||||
android:priority="50">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="data"/>
|
||||
<data android:host="*"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="data" />
|
||||
<data android:host="*" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:pathPattern=".*\\.gpx" />
|
||||
<data android:pathPattern=".*\\..*\\.gpx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.gpx" />
|
||||
@@ -110,12 +113,14 @@ limitations under the License.
|
||||
android:priority="50">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="data"/>
|
||||
<data android:host="*"/>
|
||||
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="data" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*\\.gpx" />
|
||||
<data android:pathPattern=".*\\..*\\.gpx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.gpx" />
|
||||
@@ -129,9 +134,9 @@ limitations under the License.
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="application/vnd.google-earth.kmz"/>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/vnd.google-earth.kmz" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter
|
||||
@@ -139,13 +144,15 @@ limitations under the License.
|
||||
android:priority="50">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="data"/>
|
||||
<data android:host="*"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="data" />
|
||||
<data android:host="*" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:pathPattern=".*\\.kmz" />
|
||||
<data android:pathPattern=".*\\..*\\.kmz" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kmz" />
|
||||
@@ -163,12 +170,14 @@ limitations under the License.
|
||||
android:priority="50">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="data"/>
|
||||
<data android:host="*"/>
|
||||
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="data" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*\\.kmz" />
|
||||
<data android:pathPattern=".*\\..*\\.kmz" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kmz" />
|
||||
@@ -182,9 +191,9 @@ limitations under the License.
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="application/vnd.google-earth.kml+xml"/>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/vnd.google-earth.kml+xml" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter
|
||||
@@ -192,13 +201,15 @@ limitations under the License.
|
||||
android:priority="50">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="data"/>
|
||||
<data android:host="*"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="data" />
|
||||
<data android:host="*" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:pathPattern=".*\\.kml" />
|
||||
<data android:pathPattern=".*\\..*\\.kml" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kml" />
|
||||
@@ -216,12 +227,14 @@ limitations under the License.
|
||||
android:priority="50">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="file"/>
|
||||
<data android:scheme="content"/>
|
||||
<data android:scheme="data"/>
|
||||
<data android:host="*"/>
|
||||
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:scheme="data" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*\\.kml" />
|
||||
<data android:pathPattern=".*\\..*\\.kml" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.kml" />
|
||||
@@ -237,7 +250,7 @@ limitations under the License.
|
||||
|
||||
<activity android:name=".MarkerDetailActivity">
|
||||
<intent-filter>
|
||||
<action android:name="de.dennisguse.opentracks.MarkerDetails"/>
|
||||
<action android:name="de.dennisguse.opentracks.MarkerDetails" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
@@ -246,16 +259,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:launchMode="singleTop">
|
||||
android:name=".TrackListActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/SplashTheme">
|
||||
<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 +290,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 final 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,14 @@ import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewParent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import de.dennisguse.opentracks.content.TrackDataHub;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
@@ -75,8 +78,10 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
||||
|
||||
trackDataHub = new TrackDataHub(this);
|
||||
|
||||
viewBinding.trackDetailActivityViewPager.setAdapter(new CustomFragmentPagerAdapter(getSupportFragmentManager(), FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT));
|
||||
viewBinding.trackDetailActivityTablayout.setupWithViewPager(viewBinding.trackDetailActivityViewPager);
|
||||
CustomFragmentPagerAdapter pagerAdapter = new CustomFragmentPagerAdapter(this);
|
||||
viewBinding.trackDetailActivityViewPager.setAdapter(pagerAdapter);
|
||||
new TabLayoutMediator(viewBinding.trackDetailActivityTablayout, viewBinding.trackDetailActivityViewPager,
|
||||
(tab, position) -> tab.setText(pagerAdapter.getPageTitle(position))).attach();
|
||||
if (savedInstanceState != null) {
|
||||
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
|
||||
}
|
||||
@@ -188,7 +193,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDeleted() {
|
||||
protected void onTrackDeleted() {
|
||||
runOnUiThread(this::finish);
|
||||
}
|
||||
|
||||
@@ -220,20 +225,15 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomFragmentPagerAdapter extends FragmentPagerAdapter {
|
||||
private class CustomFragmentPagerAdapter extends FragmentStateAdapter {
|
||||
|
||||
public CustomFragmentPagerAdapter(@NonNull FragmentManager fm, int behavior) {
|
||||
super(fm, behavior);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return 4;
|
||||
public CustomFragmentPagerAdapter(@NonNull FragmentActivity fa) {
|
||||
super(fa);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return StatisticsRecordedFragment.newInstance(trackId);
|
||||
@@ -249,6 +249,10 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
public CharSequence getPageTitle(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
|
||||
@@ -9,13 +9,14 @@ 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;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import de.dennisguse.opentracks.content.TrackDataHub;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
@@ -42,7 +43,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
|
||||
*/
|
||||
//NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key.
|
||||
//This mode of communication should be removed anyhow.
|
||||
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface {
|
||||
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, TrackController.Callback {
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
|
||||
@@ -126,36 +127,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);
|
||||
@@ -179,13 +150,15 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||
trackDataHub = new TrackDataHub(this);
|
||||
|
||||
viewBinding.trackDetailActivityViewPager.setAdapter(new CustomFragmentPagerAdapter(getSupportFragmentManager(), FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT));
|
||||
viewBinding.trackDetailActivityTablayout.setupWithViewPager(viewBinding.trackDetailActivityViewPager);
|
||||
CustomFragmentPagerAdapter pagerAdapter = new CustomFragmentPagerAdapter(this);
|
||||
viewBinding.trackDetailActivityViewPager.setAdapter(pagerAdapter);
|
||||
new TabLayoutMediator(viewBinding.trackDetailActivityTablayout, viewBinding.trackDetailActivityViewPager,
|
||||
(tab, position) -> tab.setText(pagerAdapter.getPageTitle(position))).attach();
|
||||
if (savedInstanceState != null) {
|
||||
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,20 +336,41 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils);
|
||||
}
|
||||
|
||||
private class CustomFragmentPagerAdapter extends FragmentPagerAdapter {
|
||||
|
||||
public CustomFragmentPagerAdapter(@NonNull FragmentManager fm, int behavior) {
|
||||
super(fm, behavior);
|
||||
@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 int getCount() {
|
||||
return 4;
|
||||
@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 FragmentStateAdapter {
|
||||
|
||||
public CustomFragmentPagerAdapter(@NonNull FragmentActivity fa) {
|
||||
super(fa);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return StatisticsRecordingFragment.newInstance();
|
||||
@@ -392,6 +386,10 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
public CharSequence getPageTitle(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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;
|
||||
@@ -20,10 +19,10 @@ import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
|
||||
|
||||
public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.Interval> {
|
||||
|
||||
private StackMode stackMode;
|
||||
private boolean metricUnits;
|
||||
private final StackMode stackMode;
|
||||
private final boolean metricUnits;
|
||||
private float sumDistance_m;
|
||||
private String category;
|
||||
private final String category;
|
||||
|
||||
public IntervalStatisticsAdapter(Context context, List<IntervalStatistics.Interval> intervalList, String category, StackMode stackMode) {
|
||||
super(context, R.layout.interval_stats_list_item, intervalList);
|
||||
@@ -61,15 +60,12 @@ public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.I
|
||||
viewHolder.distance.setText(StringUtils.formatDistance(getContext(), sumDistance_m, metricUnits));
|
||||
|
||||
if (PreferencesUtils.isReportSpeed(getContext(), category)) {
|
||||
Pair<String, String> speedParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed_ms(), metricUnits, true);
|
||||
viewHolder.rate.setText(speedParts.first + " " + speedParts.second);
|
||||
viewHolder.rate.setText(StringUtils.formatSpeed(getContext(), interval.getSpeed_ms(), metricUnits, true));
|
||||
} else {
|
||||
Pair<String, String> paceParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed_ms(), metricUnits, false);
|
||||
viewHolder.rate.setText(paceParts.first + " " + paceParts.second);
|
||||
viewHolder.rate.setText(StringUtils.formatSpeed(getContext(), interval.getSpeed_ms(), metricUnits, false));
|
||||
}
|
||||
|
||||
Pair<String, String> gainParts = StringUtils.formatElevation(getContext(), interval.getGain_m(), metricUnits);
|
||||
viewHolder.gain.setText(gainParts.first + " " + gainParts.second);
|
||||
viewHolder.gain.setText(StringUtils.formatDistance(getContext(), interval.getGain_m(), metricUnits));
|
||||
|
||||
return intervalView;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ abstract class ChartValueSeries {
|
||||
* @param fillColor the fill color
|
||||
* @param strokeColor the stroke color
|
||||
*/
|
||||
ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, int metricTitleId, int imperialTitleId, int fillColor, int strokeColor) {
|
||||
ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, int metricTitleId, int imperialTitleId, int fillColor, int strokeColor, int fontSizeSmall, int fontSizeMedium) {
|
||||
this.absoluteMin = absoluteMin;
|
||||
this.absoluteMax = absoluteMax;
|
||||
this.intervalValues = intervalValues;
|
||||
@@ -85,16 +85,14 @@ abstract class ChartValueSeries {
|
||||
strokePaint.setColor(context.getResources().getColor(strokeColor));
|
||||
strokePaint.setAntiAlias(true);
|
||||
|
||||
float scale = context.getResources().getDisplayMetrics().density;
|
||||
|
||||
// Make copies of the stroke paint with the default thickness
|
||||
titlePaint = new Paint(strokePaint);
|
||||
titlePaint.setTextSize(ChartView.MEDIUM_TEXT_SIZE * scale);
|
||||
titlePaint.setTextSize(fontSizeMedium);
|
||||
titlePaint.setTextAlign(Align.CENTER);
|
||||
titlePaint.setStyle(Style.FILL_AND_STROKE);
|
||||
|
||||
markerPaint = new Paint(strokePaint);
|
||||
markerPaint.setTextSize(ChartView.SMALL_TEXT_SIZE * scale);
|
||||
markerPaint.setTextSize(fontSizeSmall);
|
||||
markerPaint.setTextAlign(Align.RIGHT);
|
||||
markerPaint.setStyle(Style.FILL_AND_STROKE);
|
||||
|
||||
|
||||
@@ -30,9 +30,11 @@ import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.View;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.Scroller;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.GestureDetectorCompat;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
@@ -58,9 +60,6 @@ import de.dennisguse.opentracks.util.UnitConversions;
|
||||
*/
|
||||
public class ChartView extends View {
|
||||
|
||||
static final float MEDIUM_TEXT_SIZE = 18f;
|
||||
static final float SMALL_TEXT_SIZE = 12f;
|
||||
|
||||
static final int Y_AXIS_INTERVALS = 5;
|
||||
|
||||
private static final int TARGET_X_AXIS_INTERVALS = 4;
|
||||
@@ -114,7 +113,7 @@ public class ChartView extends View {
|
||||
private int effectiveWidth = 0;
|
||||
private int effectiveHeight = 0;
|
||||
|
||||
private boolean chartByDistance;
|
||||
private final boolean chartByDistance;
|
||||
private boolean metricUnits = true;
|
||||
private boolean reportSpeed = true;
|
||||
private boolean showPointer = false;
|
||||
@@ -192,6 +191,9 @@ public class ChartView extends View {
|
||||
public ChartView(Context context, final boolean chartByDistance) {
|
||||
super(context);
|
||||
this.chartByDistance = chartByDistance;
|
||||
int fontSizeSmall = ThemeUtils.getFontSizeSmallInPx(context);
|
||||
int fontSizeMedium = ThemeUtils.getFontSizeMediumInPx(context);
|
||||
|
||||
seriesList.add(new ChartValueSeries(context,
|
||||
Integer.MIN_VALUE,
|
||||
Integer.MAX_VALUE,
|
||||
@@ -199,7 +201,9 @@ public class ChartView extends View {
|
||||
R.string.description_elevation_metric,
|
||||
R.string.description_elevation_imperial,
|
||||
R.color.chart_elevation_fill,
|
||||
R.color.chart_elevation_border) {
|
||||
R.color.chart_elevation_border,
|
||||
fontSizeSmall,
|
||||
fontSizeMedium) {
|
||||
@Override
|
||||
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getElevation();
|
||||
@@ -218,7 +222,9 @@ public class ChartView extends View {
|
||||
R.string.description_speed_metric,
|
||||
R.string.description_speed_imperial,
|
||||
R.color.chart_speed_fill,
|
||||
R.color.chart_speed_border) {
|
||||
R.color.chart_speed_border,
|
||||
fontSizeSmall,
|
||||
fontSizeMedium) {
|
||||
@Override
|
||||
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getSpeed();
|
||||
@@ -238,7 +244,9 @@ public class ChartView extends View {
|
||||
R.string.description_pace_metric,
|
||||
R.string.description_pace_imperial,
|
||||
R.color.chart_pace_fill,
|
||||
R.color.chart_pace_border) {
|
||||
R.color.chart_pace_border,
|
||||
fontSizeSmall,
|
||||
fontSizeMedium) {
|
||||
@Override
|
||||
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getPace();
|
||||
@@ -258,7 +266,9 @@ public class ChartView extends View {
|
||||
R.string.description_sensor_heart_rate,
|
||||
R.string.description_sensor_heart_rate,
|
||||
R.color.chart_heart_rate_fill,
|
||||
R.color.chart_heart_rate_border) {
|
||||
R.color.chart_heart_rate_border,
|
||||
fontSizeSmall,
|
||||
fontSizeMedium) {
|
||||
@Override
|
||||
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getHeartRate();
|
||||
@@ -277,7 +287,9 @@ public class ChartView extends View {
|
||||
R.string.description_sensor_cadence,
|
||||
R.string.description_sensor_cadence,
|
||||
R.color.chart_cadence_fill,
|
||||
R.color.chart_cadence_border) {
|
||||
R.color.chart_cadence_border,
|
||||
fontSizeSmall,
|
||||
fontSizeMedium) {
|
||||
@Override
|
||||
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getCadence();
|
||||
@@ -295,7 +307,9 @@ public class ChartView extends View {
|
||||
R.string.description_sensor_power,
|
||||
R.string.description_sensor_power,
|
||||
R.color.chart_power_fill,
|
||||
R.color.chart_power_border) {
|
||||
R.color.chart_power_border,
|
||||
fontSizeSmall,
|
||||
fontSizeMedium) {
|
||||
@Override
|
||||
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
|
||||
return chartPoint.getPower();
|
||||
@@ -309,13 +323,11 @@ public class ChartView extends View {
|
||||
|
||||
backgroundColor = ThemeUtils.getBackgroundColor(context);
|
||||
|
||||
float scale = context.getResources().getDisplayMetrics().density;
|
||||
|
||||
axisPaint = new Paint();
|
||||
axisPaint.setStyle(Style.FILL_AND_STROKE);
|
||||
axisPaint.setColor(ThemeUtils.getTextColorPrimary(context));
|
||||
axisPaint.setAntiAlias(true);
|
||||
axisPaint.setTextSize(SMALL_TEXT_SIZE * scale);
|
||||
axisPaint.setTextSize(fontSizeSmall);
|
||||
|
||||
xAxisMarkerPaint = new Paint(axisPaint);
|
||||
xAxisMarkerPaint.setTextAlign(Align.CENTER);
|
||||
@@ -330,7 +342,7 @@ public class ChartView extends View {
|
||||
markerPaint.setStyle(Style.STROKE);
|
||||
markerPaint.setAntiAlias(false);
|
||||
|
||||
pointer = context.getResources().getDrawable(R.drawable.ic_logo_color_24dp);
|
||||
pointer = ContextCompat.getDrawable(context, R.drawable.ic_logo_color_24dp);
|
||||
pointer.setBounds(0, 0, pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight());
|
||||
|
||||
markerPin = MarkerUtils.getDefaultPhoto(context);
|
||||
@@ -489,6 +501,18 @@ public class ChartView extends View {
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle parent's view disallow touch event.
|
||||
*
|
||||
* @param disallow Does disallow parent touch event?
|
||||
*/
|
||||
private void requestDisallowInterceptTouchEventInParent(boolean disallow) {
|
||||
ViewParent parent = getParent();
|
||||
if (parent != null) {
|
||||
parent.requestDisallowInterceptTouchEvent(disallow);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the view horizontally by a given amount.
|
||||
*
|
||||
@@ -496,13 +520,15 @@ public class ChartView extends View {
|
||||
*/
|
||||
private void scrollBy(int deltaX) {
|
||||
int scrollX = getScrollX() + deltaX;
|
||||
if (scrollX < 0) {
|
||||
if (scrollX <= 0) {
|
||||
scrollX = 0;
|
||||
}
|
||||
|
||||
int maxWidth = effectiveWidth * (zoomLevel - 1);
|
||||
if (scrollX > maxWidth) {
|
||||
if (scrollX >= maxWidth) {
|
||||
scrollX = maxWidth;
|
||||
}
|
||||
|
||||
scrollTo(scrollX, 0);
|
||||
}
|
||||
|
||||
@@ -527,6 +553,10 @@ public class ChartView extends View {
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
boolean isZoom = detectorZoom.onTouchEvent(event);
|
||||
boolean isScrollTab = detectorScrollFlingTab.onTouchEvent(event);
|
||||
|
||||
// ChartView handles zoom gestures (more than one pointer) and all gestures when zoomed itself
|
||||
requestDisallowInterceptTouchEventInParent(event.getPointerCount() != 1 || zoomLevel != MIN_ZOOM_LEVEL);
|
||||
|
||||
return isZoom || isScrollTab;
|
||||
}
|
||||
|
||||
@@ -881,6 +911,7 @@ public class ChartView extends View {
|
||||
|
||||
/**
|
||||
* Finds the index of the first data point containing data for a series.
|
||||
*
|
||||
* @return -1 if no data point contains data for the series.
|
||||
*/
|
||||
private int getFirstPopulatedChartDataIndex(ChartValueSeries chartValueSeries) {
|
||||
|
||||
@@ -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()) {
|
||||
@@ -518,7 +488,7 @@ public class ContentProviderUtils {
|
||||
private void deleteMarkerPhoto(Context context, Marker marker) {
|
||||
if (marker != null && marker.hasPhoto()) {
|
||||
Uri uri = marker.getPhotoURI();
|
||||
File file = FileUtils.getPhotoFileIfExists(context, marker.getTrackId(), uri);
|
||||
File file = FileUtils.buildInternalPhotoFile(context, marker.getTrackId(), uri);
|
||||
if (file.exists()) {
|
||||
File parent = file.getParentFile();
|
||||
file.delete();
|
||||
@@ -646,7 +616,7 @@ public class ContentProviderUtils {
|
||||
* @param trackId the trackPoints id
|
||||
* @return the number of trackPoints inserted
|
||||
*/
|
||||
//TODO Only used for testing and file import; might be better to replace it.
|
||||
//TODO Only used for testing; might be better to replace it.
|
||||
public int bulkInsertTrackPoint(TrackPoint[] trackPoints, Track.Id trackId) {
|
||||
ContentValues[] values = new ContentValues[trackPoints.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
@@ -655,6 +625,15 @@ public class ContentProviderUtils {
|
||||
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
|
||||
}
|
||||
|
||||
//TODO Only used for file import; might be better to replace it.
|
||||
public int bulkInsertTrackPoint(List<TrackPoint> trackPoints, Track.Id trackId) {
|
||||
ContentValues[] values = new ContentValues[trackPoints.size()];
|
||||
for (int i = 0; i < trackPoints.size(); i++) {
|
||||
values[i] = createContentValues(trackPoints.get(i), trackId);
|
||||
}
|
||||
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first location id for a track.
|
||||
* Returns null if it doesn't exist.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
|
||||
protected IntervalStatisticsAdapter.StackMode stackModeListView;
|
||||
private IntervalStatisticsModel.IntervalOption selectedInterval;
|
||||
|
||||
private String intervalUnit;
|
||||
private boolean metricUnits;
|
||||
private IntervalStatisticsAdapter adapter;
|
||||
private ArrayAdapter<IntervalStatisticsModel.IntervalOption> spinnerAdapter;
|
||||
|
||||
@@ -51,7 +51,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
|
||||
|
||||
protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||
intervalUnit = PreferencesUtils.isMetricUnits(getContext()) ? getContext().getString(R.string.unit_kilometer) : getContext().getString(R.string.unit_mile);
|
||||
metricUnits = PreferencesUtils.isMetricUnits(getContext());
|
||||
if (adapter != null) {
|
||||
adapter.notifyDataSetChanged();
|
||||
spinnerAdapter.notifyDataSetChanged();
|
||||
@@ -74,7 +74,7 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
|
||||
intervalUnit = PreferencesUtils.isMetricUnits(getContext()) ? getContext().getString(R.string.unit_kilometer) : getContext().getString(R.string.unit_mile);
|
||||
metricUnits = PreferencesUtils.isMetricUnits(getContext());
|
||||
|
||||
viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
|
||||
|
||||
@@ -87,15 +87,17 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
TextView v = (TextView) super.getView(position, convertView, parent);
|
||||
v.setText(v.getText() + " " + intervalUnit);
|
||||
if (metricUnits) {
|
||||
v.setText(getContext().getString(R.string.value_integer_kilometer, Integer.parseInt(v.getText().toString())));
|
||||
} else {
|
||||
v.setText(getContext().getString(R.string.value_integer_mile, Integer.parseInt(v.getText().toString())));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
TextView v = (TextView) super.getDropDownView(position, convertView, parent);
|
||||
v.setText(v.getText() + " " + intervalUnit);
|
||||
return v;
|
||||
return getView(position, convertView, parent);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -149,7 +151,6 @@ public class IntervalsFragment extends Fragment implements TrackDataListener {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
|
||||
IntervalStatistics intervalStatistics = viewModel.getIntervalStats(metricUnits, selectedInterval);
|
||||
adapter = new IntervalStatisticsAdapter(getContext(), intervalStatistics.getIntervalList(), category, stackModeListView);
|
||||
viewBinding.intervalList.setAdapter(adapter);
|
||||
@@ -176,12 +177,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;
|
||||
@@ -361,7 +360,7 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
|
||||
public boolean resolve() {
|
||||
if (autoConflict == CONFLICT_NONE) {
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
viewBinding.exportProgressAlertIcon.setImageDrawable(getDrawable(R.drawable.ic_report_problem_24));
|
||||
viewBinding.exportProgressAlertIcon.setImageDrawable(ContextCompat.getDrawable(ExportActivity.this, R.drawable.ic_report_problem_24));
|
||||
viewBinding.exportProgressAlertMsg.setText(getString(R.string.export_track_already_exists_msg, track.getName()));
|
||||
setConflictVisibility(View.VISIBLE);
|
||||
return false;
|
||||
|
||||
@@ -167,7 +167,7 @@ public class KmlTrackWriter implements TrackWriter {
|
||||
@Override
|
||||
public void writeMarker(Marker marker) {
|
||||
if (printWriter != null && exportTrackDetail) {
|
||||
boolean existsPhoto = FileUtils.getPhotoFileIfExists(context, marker.getTrackId(), marker.getPhotoURI()) != null;
|
||||
boolean existsPhoto = FileUtils.buildInternalPhotoFile(context, marker.getTrackId(), marker.getPhotoURI()) != null;
|
||||
if (marker.hasPhoto() && exportPhotos && existsPhoto) {
|
||||
float heading = getHeading(marker.getTrackId(), marker.getLocation());
|
||||
writePhotoOverlay(marker, heading);
|
||||
|
||||
@@ -53,7 +53,7 @@ public class KmzTrackExporter implements TrackExporter {
|
||||
private final FileTrackExporter fileTrackExporter;
|
||||
|
||||
private final boolean exportPhotos;
|
||||
private Context context;
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -108,7 +108,7 @@ public class KmzTrackExporter implements TrackExporter {
|
||||
Marker marker = contentProviderUtils.createMarker(cursor);
|
||||
if (marker.hasPhoto()) {
|
||||
Uri uriPhoto = marker.getPhotoURI();
|
||||
boolean existsPhoto = FileUtils.getPhotoFileIfExists(context, track.getId(), uriPhoto) != null;
|
||||
boolean existsPhoto = FileUtils.buildInternalPhotoFile(context, track.getId(), uriPhoto) != null;
|
||||
if (existsPhoto) {
|
||||
addImage(context, zipOutputStream, uriPhoto, marker);
|
||||
}
|
||||
|
||||
+29
-45
@@ -29,7 +29,6 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
@@ -61,14 +60,10 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
|
||||
private static final String TAG = AbstractFileTrackImporter.class.getSimpleName();
|
||||
|
||||
// The maximum number of buffered locations for bulk-insertion
|
||||
private static final int MAX_BUFFERED_LOCATIONS = 512;
|
||||
|
||||
private final Context context;
|
||||
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 +99,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;
|
||||
@@ -133,18 +123,15 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
|
||||
saxParser.parse(inputStream, this);
|
||||
Log.d(TAG, "Total import time: " + (System.currentTimeMillis() - start) + "ms");
|
||||
if (trackIds.size() != 1) {
|
||||
// TODO Multi track is not supported yet.
|
||||
throw new ImportParserException("Multi track not supported");
|
||||
}
|
||||
return trackIds.get(0);
|
||||
} catch (IOException | SAXException | ParserConfigurationException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
cleanImport();
|
||||
if (trackIds.size() > 0) {
|
||||
cleanImport();
|
||||
}
|
||||
throw new ImportParserException(e);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
cleanImport();
|
||||
throw new ImportAlreadyExistsException(e);
|
||||
}
|
||||
}
|
||||
@@ -181,6 +168,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,26 +221,12 @@ 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);
|
||||
}
|
||||
trackIds.add(trackId);
|
||||
trackData.track.setId(trackId);
|
||||
}
|
||||
|
||||
/**
|
||||
* On track end.
|
||||
*/
|
||||
protected void onTrackEnd() {
|
||||
flushLocations(trackData);
|
||||
if (name != null) {
|
||||
trackData.track.setName(name);
|
||||
}
|
||||
@@ -279,17 +256,27 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
}
|
||||
trackData.track.setTrackStatistics(trackData.trackStatisticsUpdater.getTrackStatistics());
|
||||
|
||||
try {
|
||||
contentProviderUtils.updateTrack(trackData.track);
|
||||
} catch (SQLiteConstraintException e) {
|
||||
Track track = contentProviderUtils.getTrack(trackData.track.getUuid());
|
||||
if (track != null) {
|
||||
if (PreferencesUtils.getPreventReimportTracks(context)) {
|
||||
throw e;
|
||||
throw new ImportAlreadyExistsException(context.getString(R.string.import_prevent_reimport));
|
||||
}
|
||||
|
||||
//TODO This is a workaround until we have proper UI.
|
||||
trackData.track.setUuid(UUID.randomUUID());
|
||||
contentProviderUtils.updateTrack(trackData.track);
|
||||
}
|
||||
|
||||
if (trackIds.size() > 0) {
|
||||
// TODO Multi track is not supported yet.
|
||||
cleanImport();
|
||||
throw new ImportParserException("Multi track not supported");
|
||||
}
|
||||
Uri uri = contentProviderUtils.insertTrack(trackData.track);
|
||||
Track.Id trackId = new Track.Id(Long.parseLong(uri.getLastPathSegment()));
|
||||
trackIds.add(trackId);
|
||||
trackData.track.setId(trackId);
|
||||
|
||||
flushLocations(trackData);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -314,7 +301,8 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
TrackPoint trackPoint = createTrackPoint();
|
||||
|
||||
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
|
||||
throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
|
||||
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getLocation());
|
||||
return;
|
||||
}
|
||||
Marker marker = new Marker(trackPoint.getLocation());
|
||||
|
||||
@@ -401,7 +389,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 +400,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;
|
||||
@@ -493,13 +481,9 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
}
|
||||
trackData.trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
|
||||
|
||||
trackData.bufferedTrackPoints[trackData.numBufferedTrackPoints] = trackPoint;
|
||||
trackData.bufferedTrackPoints.add(trackPoint);
|
||||
trackData.numBufferedTrackPoints++;
|
||||
trackData.numberOfLocations++;
|
||||
|
||||
if (trackData.numBufferedTrackPoints >= MAX_BUFFERED_LOCATIONS) {
|
||||
flushLocations(trackData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -511,7 +495,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
if (data.numBufferedTrackPoints <= 0) {
|
||||
return;
|
||||
}
|
||||
contentProviderUtils.bulkInsertTrackPoint(Arrays.copyOfRange(data.bufferedTrackPoints, 0, data.numBufferedTrackPoints), data.track.getId());
|
||||
contentProviderUtils.bulkInsertTrackPoint(data.bufferedTrackPoints, data.track.getId());
|
||||
data.numBufferedTrackPoints = 0;
|
||||
}
|
||||
|
||||
@@ -548,7 +532,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
||||
final long importTime = System.currentTimeMillis();
|
||||
|
||||
// The buffered locations
|
||||
final TrackPoint[] bufferedTrackPoints = new TrackPoint[MAX_BUFFERED_LOCATIONS];
|
||||
final List<TrackPoint> bufferedTrackPoints = new ArrayList<>();
|
||||
|
||||
// The number of buffered locations
|
||||
int numBufferedTrackPoints = 0;
|
||||
|
||||
@@ -24,6 +24,7 @@ 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;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
@@ -110,7 +111,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 +131,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() {
|
||||
@@ -161,12 +162,12 @@ public class ImportActivity extends FragmentActivity {
|
||||
viewBinding.importProgressLeftButton.setVisibility(View.VISIBLE);
|
||||
viewBinding.importProgressLeftButton.setText(getString(R.string.generic_show_errors));
|
||||
viewBinding.importProgressLeftButton.setOnClickListener((view) -> ErrorListDialog.showDialog(getSupportFragmentManager(), getString(R.string.import_error_list_dialog_title), summary.getFileErrors()));
|
||||
viewBinding.importProgressAlertIcon.setImageDrawable(getDrawable(R.drawable.ic_report_problem_24));
|
||||
viewBinding.importProgressAlertIcon.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_report_problem_24));
|
||||
String msg = getResources().getQuantityString(R.plurals.generic_completed_with_errors, summary.getErrorCount(), summary.getErrorCount());
|
||||
viewBinding.importProgressAlertMsg.setText(msg);
|
||||
} else {
|
||||
viewBinding.importProgressLeftButton.setVisibility(View.GONE);
|
||||
viewBinding.importProgressAlertIcon.setImageDrawable(getDrawable(R.drawable.ic_dialog_success_24dp));
|
||||
viewBinding.importProgressAlertIcon.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_dialog_success_24dp));
|
||||
viewBinding.importProgressAlertMsg.setText(getString(R.string.generic_completed));
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -4,4 +4,8 @@ class ImportAlreadyExistsException extends RuntimeException {
|
||||
public ImportAlreadyExistsException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
|
||||
public ImportAlreadyExistsException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,12 +63,12 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
|
||||
private boolean trackStarted = false;
|
||||
private String extendedDataType;
|
||||
private ArrayList<TrackPoint> trackPoints = new ArrayList<>();
|
||||
private ArrayList<Float> speedList = new ArrayList<>();
|
||||
private ArrayList<Float> cadenceList = new ArrayList<>();
|
||||
private ArrayList<Float> heartRateList = new ArrayList<>();
|
||||
private ArrayList<Float> powerList = new ArrayList<>();
|
||||
private ArrayList<Float> elevationGainList = new ArrayList<>();
|
||||
private final ArrayList<TrackPoint> trackPoints = new ArrayList<>();
|
||||
private final ArrayList<Float> speedList = new ArrayList<>();
|
||||
private final ArrayList<Float> cadenceList = new ArrayList<>();
|
||||
private final ArrayList<Float> heartRateList = new ArrayList<>();
|
||||
private final ArrayList<Float> powerList = new ArrayList<>();
|
||||
private final ArrayList<Float> elevationGainList = new ArrayList<>();
|
||||
|
||||
public KmlFileTrackImporter(Context context) {
|
||||
this(context, new ContentProviderUtils(context));
|
||||
@@ -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,12 +171,16 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
zipInputStream.closeEntry();
|
||||
}
|
||||
if (trackId == null) {
|
||||
Log.d(TAG, "Unable to find doc.kml in kmz");
|
||||
throw new RuntimeException(context.getString(R.string.import_no_kml_file_found));
|
||||
}
|
||||
return trackId;
|
||||
} catch (ImportParserException | ImportAlreadyExistsException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
@@ -283,14 +275,15 @@ public class KmzTrackImporter implements TrackImporter {
|
||||
* Reads an image file (zipInputStream) and save it in a file called fileName inside photo folder.
|
||||
*
|
||||
* @param zipInputStream the zip input stream
|
||||
* @param trackId the track's id which image belongs to.
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class GpsStatus {
|
||||
|
||||
private GpsStatusValue gpsStatus = GpsStatusValue.GPS_NONE;
|
||||
private GpsStatusListener client;
|
||||
private Context context;
|
||||
private final Context context;
|
||||
|
||||
// Last location. It can be null.
|
||||
private Location lastLocation = null;
|
||||
@@ -51,7 +51,7 @@ class GpsStatus {
|
||||
}
|
||||
}
|
||||
|
||||
private Handler gpsStatusHandler;
|
||||
private final Handler gpsStatusHandler;
|
||||
private GpsStatusRunner gpsStatusRunner = null;
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,7 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
|
||||
public class HandlerServer {
|
||||
|
||||
private static String TAG = HandlerServer.class.getSimpleName();
|
||||
private static final String TAG = HandlerServer.class.getSimpleName();
|
||||
|
||||
private final LocationHandler locationHandler;
|
||||
private final HandlerServerInterface service;
|
||||
|
||||
+28
-2
@@ -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);
|
||||
|
||||
+9
@@ -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);
|
||||
@@ -129,9 +154,11 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
|
||||
activityPreferenceDialog = ActivityTypePreference.ActivityPreferenceDialog.newInstance(preference.getKey());
|
||||
dialogFragment = activityPreferenceDialog;
|
||||
} else if (preference instanceof BluetoothLeHeartRatePreference) {
|
||||
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.HEART_RATE_SERVICE_UUID);
|
||||
dialogFragment = BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(preference.getKey(), BluetoothUtils.HEART_RATE_SUPPORTING_DEVICES);
|
||||
} 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) {
|
||||
|
||||
+23
@@ -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);
|
||||
}
|
||||
}
|
||||
+30
-16
@@ -8,8 +8,6 @@ import android.bluetooth.le.ScanResult;
|
||||
import android.bluetooth.le.ScanSettings;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelUuid;
|
||||
import android.text.TextUtils;
|
||||
@@ -18,13 +16,15 @@ import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.DialogPreference;
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.settings.BluetoothLeAdapter;
|
||||
@@ -39,7 +39,7 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
|
||||
private static final String TAG = BluetoothLeSensorPreference.class.getSimpleName();
|
||||
|
||||
private static final String ARG_BLUETOOTH_UUID = "bluetoothUUID";
|
||||
private static final String ARG_BLE_SERVICE_UUIDS = "bluetoothUUID";
|
||||
|
||||
private static final int DEVICE_NONE_RESOURCEID = R.string.value_none;
|
||||
|
||||
@@ -94,6 +94,8 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
|
||||
public static class BluetoothLeSensorPreferenceDialog extends PreferenceDialogFragmentCompat {
|
||||
|
||||
private AnimatedVectorDrawableCompat bluetoothIcon;
|
||||
|
||||
private int selectedEntryIndex;
|
||||
private final BluetoothLeAdapter listAdapter = new BluetoothLeAdapter();
|
||||
|
||||
@@ -109,8 +111,9 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
|
||||
@Override
|
||||
public void onBatchScanResults(List<ScanResult> results) {
|
||||
for (ScanResult scanResult : results) {
|
||||
listAdapter.add(scanResult.getDevice());
|
||||
for (ScanResult result : results) {
|
||||
Log.d(TAG, "Found device " + result.getDevice().getName() + " " + result);
|
||||
listAdapter.add(result.getDevice());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,10 +127,14 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
};
|
||||
|
||||
public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, UUID sensorUUID) {
|
||||
return newInstance(preferenceKey, Collections.singletonList(sensorUUID));
|
||||
}
|
||||
|
||||
public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, List<UUID> sensorUUIDs) {
|
||||
final BluetoothLeSensorPreferenceDialog fragment = new BluetoothLeSensorPreferenceDialog();
|
||||
final Bundle b = new Bundle(1);
|
||||
b.putString(ARG_KEY, preferenceKey);
|
||||
b.putSerializable(ARG_BLUETOOTH_UUID, sensorUUID);
|
||||
b.putParcelableArrayList(ARG_BLE_SERVICE_UUIDS, new ArrayList<>(sensorUUIDs.stream().map(ParcelUuid::new).collect(Collectors.toList())));
|
||||
fragment.setArguments(b);
|
||||
return fragment;
|
||||
}
|
||||
@@ -136,8 +143,12 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
UUID sensorUUID = (UUID) getArguments().getSerializable(ARG_BLUETOOTH_UUID);
|
||||
Log.i(TAG, "UUID: " + sensorUUID);
|
||||
List<ParcelUuid> serviceUUIDs = getArguments().getParcelableArrayList(ARG_BLE_SERVICE_UUIDS);
|
||||
|
||||
// Don't know why: need to load the drawable _twice_, so that animation is actually started.
|
||||
bluetoothIcon = AnimatedVectorDrawableCompat.create(getContext(), R.drawable.ic_bluetooth_searching_animated_24dp);
|
||||
bluetoothIcon = AnimatedVectorDrawableCompat.create(getContext(), R.drawable.ic_bluetooth_searching_animated_24dp);
|
||||
bluetoothIcon.start();
|
||||
|
||||
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getAdapter(getContext());
|
||||
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
|
||||
@@ -148,7 +159,7 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
}
|
||||
|
||||
if (bluetoothAdapter.isDiscovering()) {
|
||||
Log.i(TAG, "Cancelling ongoing B<aluetooth discovery.");
|
||||
Log.i(TAG, "Cancelling ongoing Bluetooth discovery.");
|
||||
bluetoothAdapter.cancelDiscovery();
|
||||
}
|
||||
|
||||
@@ -170,9 +181,7 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
selectedEntryIndex = 1;
|
||||
}
|
||||
|
||||
ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder().setServiceUuid(new ParcelUuid(sensorUUID));
|
||||
List<ScanFilter> scanFilter = new ArrayList<>();
|
||||
scanFilter.add(scanFilterBuilder.build());
|
||||
List<ScanFilter> scanFilter = serviceUUIDs.stream().map(it -> new ScanFilter.Builder().setServiceUuid(it).build()).collect(Collectors.toList());
|
||||
|
||||
ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder();
|
||||
scanSettingsBuilder.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY);
|
||||
@@ -193,9 +202,7 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
Drawable icon = ContextCompat.getDrawable(getContext(), R.drawable.ic_bluetooth_searching_animated_24dp);
|
||||
((Animatable) icon).start();
|
||||
builder.setIcon(icon);
|
||||
builder.setIcon(bluetoothIcon);
|
||||
|
||||
builder.setPositiveButton(null, null);
|
||||
}
|
||||
@@ -214,5 +221,12 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
bluetoothIcon = null;
|
||||
scanner = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||
@@ -39,8 +42,17 @@ 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 List<UUID> HEART_RATE_SUPPORTING_DEVICES = Collections.unmodifiableList(Arrays.asList(
|
||||
BluetoothUtils.HEART_RATE_SERVICE_UUID,
|
||||
//Devices that support HEART_RATE_SERVICE_UUID, but do not announce HEART_RATE_SERVICE_UUID in there BLE announcement messages (during device discovery).
|
||||
UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb") //Miband3
|
||||
));
|
||||
|
||||
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 +74,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 +91,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.
|
||||
*
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
|
||||
@@ -15,6 +16,6 @@ public class MarkerUtils {
|
||||
}
|
||||
|
||||
public static Drawable getDefaultPhoto(@NonNull Context context) {
|
||||
return context.getResources().getDrawable(ICON_ID);
|
||||
return ContextCompat.getDrawable(context, ICON_ID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,18 +5,18 @@ import android.hardware.SensorManager;
|
||||
public class PressureSensorUtils {
|
||||
|
||||
//Everything above is considered a meaningful change in elevation.
|
||||
private static float ELEVATION_CHANGE_DIFF_M = 3.0f;
|
||||
private static final float ELEVATION_CHANGE_DIFF_M = 3.0f;
|
||||
|
||||
private static float EXPONENTIAL_SMOOTHING = 0.3f;
|
||||
private static final float EXPONENTIAL_SMOOTHING = 0.3f;
|
||||
|
||||
private PressureSensorUtils() {
|
||||
}
|
||||
|
||||
public static class ElevationChange {
|
||||
|
||||
private float currentSensorValue_hPa;
|
||||
private final float currentSensorValue_hPa;
|
||||
|
||||
private float elevationChange_m;
|
||||
private final float elevationChange_m;
|
||||
|
||||
public ElevationChange(float currentSensorValue_hPa, float elevationChange_m) {
|
||||
this.currentSensorValue_hPa = currentSensorValue_hPa;
|
||||
|
||||
@@ -118,6 +118,34 @@ public class StringUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatSpeed(Context context, double speed_mps, boolean metricUnits, boolean reportSpeed) {
|
||||
if (Double.isNaN(speed_mps) || Double.isInfinite(speed_mps)) {
|
||||
speed_mps = 0;
|
||||
}
|
||||
|
||||
double speed = speed_mps * UnitConversions.M_TO_KM; //KM p sec
|
||||
if (!metricUnits) {
|
||||
speed *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
if (reportSpeed) {
|
||||
if (metricUnits) {
|
||||
return context.getString(R.string.value_float_kilometer_hour, speed * UnitConversions.S_TO_HR);
|
||||
} else {
|
||||
return context.getString(R.string.value_float_mile_hour, speed * UnitConversions.S_TO_HR);
|
||||
}
|
||||
} else {
|
||||
int pace = speed == 0 ? 0 : (int) Math.round(1 / speed); //sec / [KM | MI]
|
||||
int minutes = pace / 60;
|
||||
int seconds = pace % 60;
|
||||
if (metricUnits) {
|
||||
return context.getString(R.string.value_pace_kilometer, minutes, seconds);
|
||||
} else {
|
||||
return context.getString(R.string.value_pace_mile, minutes, seconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatDecimal(double value) {
|
||||
return StringUtils.formatDecimal(value, 2);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
|
||||
public class ThemeUtils {
|
||||
|
||||
private ThemeUtils() {
|
||||
@@ -37,4 +40,18 @@ public class ThemeUtils {
|
||||
|
||||
return ContextCompat.getColor(context, typedValue.resourceId);
|
||||
}
|
||||
|
||||
public static int getFontSizeSmallInPx(Context context) {
|
||||
TypedArray typedArray = context.obtainStyledAttributes(R.style.TextSmall, new int[]{android.R.attr.textSize});
|
||||
int fontSize = typedArray.getDimensionPixelSize(0, 12);
|
||||
typedArray.recycle();
|
||||
return fontSize;
|
||||
}
|
||||
|
||||
public static int getFontSizeMediumInPx(Context context) {
|
||||
TypedArray typedArray = context.obtainStyledAttributes(R.style.TextMedium, new int[]{android.R.attr.textSize});
|
||||
int fontSize = typedArray.getDimensionPixelSize(0, 15);
|
||||
typedArray.recycle();
|
||||
return fontSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import de.dennisguse.opentracks.util.LocationUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
public class IntervalStatistics {
|
||||
private List<Interval> intervalList = new ArrayList<>();
|
||||
private final List<Interval> intervalList = new ArrayList<>();
|
||||
private float distanceInterval_m;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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" />
|
||||
@@ -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,10 +19,10 @@ 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">
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/track_controller_record"
|
||||
@@ -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>
|
||||
@@ -47,10 +47,10 @@ limitations under the License.
|
||||
android:id="@+id/track_list_arrow_long"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingRight="16dp">
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
@@ -73,7 +73,7 @@ limitations under the License.
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_toRightOf="@id/track_list_arrow_long"
|
||||
android:layout_toEndOf="@id/track_list_arrow_long"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/track_detail_activity_view_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
|
||||
@@ -20,21 +20,18 @@
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="4"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:text="@string/aggregated_stats_empty_message" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
android:id="@+id/aggregated_stats_sport_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintLeft_toLeftOf="@id/guideline1"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_activity_bike_24dp" />
|
||||
|
||||
@@ -45,10 +45,10 @@
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/aggregated_stats_sport_icon"
|
||||
app:layout_constraintLeft_toRightOf="@+id/aggregated_stats_sport_icon"
|
||||
app:layout_constraintStart_toEndOf="@+id/aggregated_stats_sport_icon"
|
||||
app:layout_constraintTop_toTopOf="@+id/aggregated_stats_sport_icon"
|
||||
tools:text="@string/activity_type_cycling"
|
||||
android:layout_marginLeft="16dp" />
|
||||
android:layout_marginStart="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/aggregated_stats_num_tracks"
|
||||
@@ -56,36 +56,36 @@
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/aggregated_stats_type_label"
|
||||
app:layout_constraintLeft_toRightOf="@+id/aggregated_stats_type_label"
|
||||
app:layout_constraintStart_toEndOf="@+id/aggregated_stats_type_label"
|
||||
app:layout_constraintTop_toTopOf="@+id/aggregated_stats_type_label"
|
||||
tools:text="(1)"
|
||||
android:layout_marginLeft="4dp" />
|
||||
android:layout_marginStart="4dp" />
|
||||
|
||||
<!-- Distance -->
|
||||
<TextView
|
||||
android:id="@+id/aggregated_stats_distance"
|
||||
style="@style/StatsLargeValue"
|
||||
app:layout_constraintLeft_toLeftOf="@id/guideline1"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline1"
|
||||
app:layout_constraintTop_toBottomOf="@+id/aggregated_stats_sport_icon"
|
||||
tools:text="100000"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/aggregated_stats_distance_unit"
|
||||
style="@style/StatsUnit"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/aggregated_stats_distance"
|
||||
app:layout_constraintLeft_toRightOf="@+id/aggregated_stats_distance"
|
||||
app:layout_constraintStart_toEndOf="@+id/aggregated_stats_distance"
|
||||
tools:text="km" />
|
||||
|
||||
<!-- Time -->
|
||||
<TextView
|
||||
android:id="@+id/aggregated_stats_time"
|
||||
style="@style/StatsLargeValue"
|
||||
app:layout_constraintRight_toRightOf="@id/guideline3"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline3"
|
||||
app:layout_constraintTop_toBottomOf="@+id/aggregated_stats_sport_icon"
|
||||
tools:text="00:00:00"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<!-- Avg. rate -->
|
||||
@@ -94,8 +94,8 @@
|
||||
style="@style/StatsSmallValue"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintTop_toBottomOf="@id/aggregated_stats_distance"
|
||||
app:layout_constraintLeft_toRightOf="@id/guideline1"
|
||||
app:layout_constraintRight_toLeftOf="@+id/aggregated_stats_avg_rate_unit"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline1"
|
||||
app:layout_constraintEnd_toStartOf="@+id/aggregated_stats_avg_rate_unit"
|
||||
android:layout_marginTop="24dp"
|
||||
tools:text="30" />
|
||||
|
||||
@@ -103,8 +103,8 @@
|
||||
android:id="@+id/aggregated_stats_avg_rate_unit"
|
||||
style="@style/StatsUnit"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/aggregated_stats_avg_rate"
|
||||
app:layout_constraintLeft_toRightOf="@+id/aggregated_stats_avg_rate"
|
||||
app:layout_constraintRight_toRightOf="@id/guideline2"
|
||||
app:layout_constraintStart_toEndOf="@+id/aggregated_stats_avg_rate"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline2"
|
||||
tools:text="km/h" />
|
||||
|
||||
<TextView
|
||||
@@ -112,8 +112,8 @@
|
||||
style="@style/StatsTinyLabel"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintTop_toBottomOf="@+id/aggregated_stats_avg_rate"
|
||||
app:layout_constraintLeft_toRightOf="@id/guideline1"
|
||||
app:layout_constraintRight_toRightOf="@id/guideline2"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline1"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline2"
|
||||
tools:text="@string/stats_average_moving_speed" />
|
||||
|
||||
<!-- Max. rate -->
|
||||
@@ -122,8 +122,8 @@
|
||||
style="@style/StatsSmallValue"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintTop_toBottomOf="@id/aggregated_stats_time"
|
||||
app:layout_constraintLeft_toRightOf="@id/guideline2"
|
||||
app:layout_constraintRight_toLeftOf="@+id/aggregated_stats_max_rate_unit"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/aggregated_stats_max_rate_unit"
|
||||
android:layout_marginTop="24dp"
|
||||
tools:text="70" />
|
||||
|
||||
@@ -131,8 +131,8 @@
|
||||
android:id="@+id/aggregated_stats_max_rate_unit"
|
||||
style="@style/StatsUnit"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/aggregated_stats_max_rate"
|
||||
app:layout_constraintLeft_toRightOf="@+id/aggregated_stats_max_rate"
|
||||
app:layout_constraintRight_toLeftOf="@id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@+id/aggregated_stats_max_rate"
|
||||
app:layout_constraintEnd_toStartOf="@id/guideline3"
|
||||
tools:text="km/h" />
|
||||
|
||||
<TextView
|
||||
@@ -140,8 +140,8 @@
|
||||
style="@style/StatsTinyLabel"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintTop_toBottomOf="@+id/aggregated_stats_max_rate"
|
||||
app:layout_constraintLeft_toRightOf="@id/guideline2"
|
||||
app:layout_constraintRight_toLeftOf="@id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline2"
|
||||
app:layout_constraintEnd_toStartOf="@id/guideline3"
|
||||
tools:text="@string/stats_max_speed" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -14,7 +14,7 @@
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_weight="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="0dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_weight="1"
|
||||
@@ -62,9 +62,9 @@
|
||||
style="@style/ProgressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="24dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/export_progress_done"
|
||||
@@ -113,8 +113,8 @@
|
||||
android:id="@+id/export_progress_apply_to_all"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="24dp"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:checked="false"
|
||||
android:text="@string/export_all_do_it_for_all"
|
||||
@@ -144,7 +144,7 @@
|
||||
<Button
|
||||
android:id="@+id/export_progress_right_button"
|
||||
style="@style/BottomButton"
|
||||
android:layout_gravity="right"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/generic_overwrite"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_weight="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="0dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_weight="1"
|
||||
@@ -60,9 +60,9 @@
|
||||
style="@style/ProgressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="24dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/import_progress_done"
|
||||
@@ -126,7 +126,7 @@
|
||||
<Button
|
||||
android:id="@+id/import_progress_right_button"
|
||||
style="@style/BottomButton"
|
||||
android:layout_gravity="right"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/generic_ok"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_margin="8dp">
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
style="@style/TextMediumPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/stats_split_interval"
|
||||
style="@style/TextMedium" />
|
||||
android:text="@string/stats_split_interval" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_intervals"
|
||||
@@ -28,56 +29,39 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_margin="8dp">
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interval_distance"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
style="@style/TextMedium"
|
||||
android:layout_margin="1dp"
|
||||
android:textAlignment="center"
|
||||
android:layout_gravity="center"
|
||||
style="@style/IntervalLabel"
|
||||
android:text="@string/stats_distance" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interval_rate"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
style="@style/TextMedium"
|
||||
android:layout_margin="1dp"
|
||||
android:textAlignment="center"
|
||||
android:layout_gravity="center"
|
||||
tools:text="Speed"/>
|
||||
style="@style/IntervalLabel"
|
||||
tools:text="Speed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interval_elevation"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
style="@style/TextMedium"
|
||||
android:layout_margin="1dp"
|
||||
android:textAlignment="center"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/stats_elevation" />
|
||||
style="@style/IntervalLabel"
|
||||
android:text="@string/stats_gain" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:background="@color/stats_separator"/>
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@color/stats_separator" />
|
||||
|
||||
<ListView
|
||||
android:id="@+id/interval_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"/>
|
||||
android:layout_margin="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interval_list_empty_view"
|
||||
|
||||
@@ -1,47 +1,31 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interval_item_distance"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center_vertical"
|
||||
style="@style/TextMedium"
|
||||
tools:text="1 km" />
|
||||
<TextView
|
||||
android:id="@+id/interval_item_distance"
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
tools:text="1 km" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interval_item_rate"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center_vertical"
|
||||
style="@style/TextMedium"
|
||||
tools:text="15 km/h" />
|
||||
<TextView
|
||||
android:id="@+id/interval_item_rate"
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
tools:text="15 km/h" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_arrow_drop_up_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/interval_item_gain"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
style="@style/TextMedium"
|
||||
tools:text="100 m" />
|
||||
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/interval_item_gain"
|
||||
style="@style/TextMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
app:drawableStartCompat="@drawable/ic_arrow_drop_up_24"
|
||||
tools:text="100 m" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -44,7 +44,8 @@ limitations under the License.
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="12dp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:shrinkColumns="1"
|
||||
android:stretchColumns="1">
|
||||
@@ -56,8 +57,8 @@ limitations under the License.
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="@string/image_track"
|
||||
app:tint="?android:textColorPrimary" />
|
||||
<!-- Table 1: Row 1: Column 2: (Table 2) -->
|
||||
@@ -105,8 +106,8 @@ limitations under the License.
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginLeft="2dp"
|
||||
android:layout_marginRight="2dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:layout_marginBottom="1dp"
|
||||
android:contentDescription="@string/image_marker_count"
|
||||
android:scaleType="centerInside"
|
||||
|
||||
@@ -116,12 +116,12 @@ limitations under the License.
|
||||
<Button
|
||||
android:id="@+id/marker_edit_cancel"
|
||||
style="@style/BottomButton"
|
||||
android:layout_gravity="left"
|
||||
android:layout_gravity="start"
|
||||
android:text="@string/generic_cancel" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/marker_edit_done"
|
||||
style="@style/BottomButton"
|
||||
android:layout_gravity="right" />
|
||||
android:layout_gravity="end" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -19,9 +19,9 @@ limitations under the License.
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingRight="8dp">
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/activity_type_preference_text_view"
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -36,8 +36,8 @@
|
||||
style="@style/StatsLargeLabel"
|
||||
android:text="@string/stats_distance"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
@@ -45,8 +45,8 @@
|
||||
style="@style/StatsLargeValue"
|
||||
android:value="@string/value_unknown"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline"
|
||||
app:layout_constraintRight_toLeftOf="@+id/stats_distance_unit"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline"
|
||||
app:layout_constraintEnd_toStartOf="@+id/stats_distance_unit"
|
||||
app:layout_constraintTop_toBottomOf="@+id/stats_distance_label"
|
||||
tools:text="100" />
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
android:id="@+id/stats_distance_unit"
|
||||
style="@style/StatsUnit"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/stats_distance_value"
|
||||
app:layout_constraintLeft_toRightOf="@+id/stats_distance_value"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toEndOf="@+id/stats_distance_value"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
tools:text="km" />
|
||||
|
||||
<!-- Activity Type -->
|
||||
@@ -64,16 +64,16 @@
|
||||
style="@style/StatsLargeLabel"
|
||||
android:text="@string/track_edit_activity_type_hint"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline2"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline3"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/stats_activity_type_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline2"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline3"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/stats_distance_value"
|
||||
app:layout_constraintTop_toTopOf="@+id/stats_distance_value"
|
||||
app:tint="?android:attr/textColorPrimary"
|
||||
@@ -85,15 +85,15 @@
|
||||
style="@style/StatsLargeLabel"
|
||||
android:text="@string/stats_total_time"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/stats_distance_value" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_total_time_value"
|
||||
style="@style/StatsLargeValue"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/stats_total_time_label"
|
||||
tools:text="00:00:00" />
|
||||
|
||||
@@ -103,8 +103,8 @@
|
||||
style="@style/StatsLargeLabel"
|
||||
android:text="@string/stats_moving_time"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline2"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline3"
|
||||
app:layout_constraintTop_toTopOf="@+id/stats_total_time_label"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/stats_total_time_label" />
|
||||
|
||||
@@ -112,8 +112,8 @@
|
||||
android:id="@+id/stats_moving_time_value"
|
||||
style="@style/StatsLargeValue"
|
||||
android:value="@string/value_unknown"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline2"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline3"
|
||||
app:layout_constraintTop_toBottomOf="@+id/stats_moving_time_label"
|
||||
tools:text="00:00:00" />
|
||||
|
||||
@@ -130,8 +130,8 @@
|
||||
android:id="@+id/stats_speed_horizontal_line"
|
||||
style="@style/StatsHorizontalLine"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline3"
|
||||
app:layout_constraintTop_toBottomOf="@+id/stats_time_barrier" />
|
||||
|
||||
<!-- Average speed/pace -->
|
||||
@@ -139,8 +139,8 @@
|
||||
android:id="@+id/stats_average_speed_label"
|
||||
style="@style/StatsSmallLabel"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline2"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline3"
|
||||
app:layout_constraintTop_toBottomOf="@id/stats_speed_horizontal_line"
|
||||
tools:text="Avg. Speed" />
|
||||
|
||||
@@ -149,8 +149,8 @@
|
||||
style="@style/StatsSmallValue"
|
||||
android:value="@string/value_unknown"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline2"
|
||||
app:layout_constraintRight_toLeftOf="@+id/stats_average_speed_unit"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/stats_average_speed_unit"
|
||||
app:layout_constraintTop_toBottomOf="@+id/stats_average_speed_label"
|
||||
tools:text="30.00" />
|
||||
|
||||
@@ -158,8 +158,8 @@
|
||||
android:id="@+id/stats_average_speed_unit"
|
||||
style="@style/StatsUnit"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/stats_average_speed_value"
|
||||
app:layout_constraintLeft_toRightOf="@+id/stats_average_speed_value"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@+id/stats_average_speed_value"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline3"
|
||||
tools:text="km/h" />
|
||||
|
||||
<!-- Speed max -->
|
||||
@@ -167,8 +167,8 @@
|
||||
android:id="@+id/stats_max_speed_label"
|
||||
style="@style/StatsSmallLabel"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintTop_toBottomOf="@id/stats_average_speed_value"
|
||||
tools:text="Max. Speed" />
|
||||
|
||||
@@ -177,8 +177,8 @@
|
||||
style="@style/StatsSmallValue"
|
||||
android:value="@string/value_unknown"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline"
|
||||
app:layout_constraintRight_toLeftOf="@+id/stats_max_speed_unit"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline"
|
||||
app:layout_constraintEnd_toStartOf="@+id/stats_max_speed_unit"
|
||||
app:layout_constraintTop_toBottomOf="@+id/stats_max_speed_label"
|
||||
tools:text="30.00" />
|
||||
|
||||
@@ -186,8 +186,8 @@
|
||||
android:id="@+id/stats_max_speed_unit"
|
||||
style="@style/StatsUnit"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/stats_max_speed_value"
|
||||
app:layout_constraintLeft_toRightOf="@+id/stats_max_speed_value"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toEndOf="@+id/stats_max_speed_value"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
tools:text="km/h" />
|
||||
|
||||
<!-- Moving speed max -->
|
||||
@@ -195,8 +195,8 @@
|
||||
android:id="@+id/stats_moving_speed_label"
|
||||
style="@style/StatsSmallLabel"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline2"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline3"
|
||||
app:layout_constraintTop_toBottomOf="@id/stats_average_speed_value"
|
||||
tools:text="Max. Moving Speed" />
|
||||
|
||||
@@ -205,8 +205,8 @@
|
||||
style="@style/StatsSmallValue"
|
||||
android:value="@string/value_unknown"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline2"
|
||||
app:layout_constraintRight_toLeftOf="@+id/stats_moving_speed_unit"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/stats_moving_speed_unit"
|
||||
app:layout_constraintTop_toBottomOf="@+id/stats_moving_speed_label"
|
||||
tools:text="30.00" />
|
||||
|
||||
@@ -214,8 +214,8 @@
|
||||
android:id="@+id/stats_moving_speed_unit"
|
||||
style="@style/StatsUnit"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/stats_moving_speed_value"
|
||||
app:layout_constraintLeft_toRightOf="@+id/stats_moving_speed_value"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@+id/stats_moving_speed_value"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline3"
|
||||
tools:text="km/h" />
|
||||
|
||||
<!-- Barrier -->
|
||||
@@ -231,8 +231,8 @@
|
||||
android:id="@+id/stats_elevation_horizontal_line"
|
||||
style="@style/StatsHorizontalLine"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline3"
|
||||
app:layout_constraintTop_toBottomOf="@+id/stats_speed_barrier" />
|
||||
|
||||
<!-- Elevation gain -->
|
||||
@@ -247,8 +247,8 @@
|
||||
style="@style/StatsSmallLabel"
|
||||
android:text="@string/stats_gain"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline2"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline3"
|
||||
app:layout_constraintTop_toBottomOf="@id/stats_elevation_horizontal_line" />
|
||||
|
||||
<TextView
|
||||
@@ -256,8 +256,8 @@
|
||||
style="@style/StatsSmallValue"
|
||||
android:value="@string/value_unknown"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline2"
|
||||
app:layout_constraintRight_toLeftOf="@+id/stats_elevation_gain_unit"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/stats_elevation_gain_unit"
|
||||
app:layout_constraintTop_toBottomOf="@+id/stats_elevation_gain_label"
|
||||
tools:text="100" />
|
||||
|
||||
@@ -265,8 +265,8 @@
|
||||
android:id="@+id/stats_elevation_gain_unit"
|
||||
style="@style/StatsUnit"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/stats_elevation_gain_value"
|
||||
app:layout_constraintLeft_toRightOf="@+id/stats_elevation_gain_value"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@+id/stats_elevation_gain_value"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline3"
|
||||
tools:text="m" />
|
||||
|
||||
<!-- Barrier -->
|
||||
@@ -282,8 +282,8 @@
|
||||
android:id="@+id/stats_end_horizontal_line"
|
||||
style="@style/StatsHorizontalLine"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintLeft_toRightOf="@+id/guideline"
|
||||
app:layout_constraintRight_toLeftOf="@+id/guideline3"
|
||||
app:layout_constraintStart_toEndOf="@+id/guideline"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline3"
|
||||
app:layout_constraintTop_toBottomOf="@+id/stats_elevation_barrier" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user