Compare commits

..

57 Commits

Author SHA1 Message Date
Dennis Guse 397e056f6f Release: v3.14.0 2020-12-10 22:52:02 +01:00
Dennis Guse eb79f2277a ChartView let's parent handle TouchEvents if not zoomed. 2020-12-06 22:35:44 +01:00
Román Martínez ae8833d21b ViewPager migrate to ViewPager2. Fixes #522.
Swiping between tabs work. Fixes #105.
2020-12-06 22:35:44 +01:00
Dennis Guse e8017a4e9d Bugfix: In preferences, the Bluetooth search icon was only animated after opening the dialog a 2nd time. 2020-12-06 00:09:24 +01:00
Dennis Guse cea180f871 Code cleanup using AndroidStudio's Analyze feature. 2020-12-05 23:22:15 +01:00
Dennis Guse be1b6ece9d IntervalsFragment: generate UI values from strings.xml (i18n). 2020-12-05 23:11:29 +01:00
Vanous 0592863d3c add Huami devices into Readme Tested Sensors 2020-12-05 15:42:09 +01:00
Dennis Guse 18bc100d5a BLE sensors: Miband3's is now considered a heart rate sensor during discovery.
Some BLE devices do not announce all there supported services in the BLE advertisement packages.
Android's ScanFilter decides based upon this information only.
When connecting to such a device (and trigger service discovery), the service can still be used.

Fixes #447.
2020-12-05 12:36:57 +01:00
Peter Storch 4476a15558 throw Exception when no doc.kml file found in kmz 2020-11-29 13:56:22 +01:00
Peter Storch 215c28be85 fix #537 ignore waypoint markers with invalid
coordinates on import.

I've also added a unit test with such an export file.
2020-11-29 13:56:22 +01:00
Dennis Guse a429fbe4df Interval list: RTL and localized formatting (only default strings.xml). 2020-11-28 15:05:49 +01:00
Dennis Guse 6b470c7fdc Intervals: change label from elevation to gain. 2020-11-28 15:05:49 +01:00
Dennis Guse 6b690f38ad Charts: text size is resolved from styles.
Fixes #524.
2020-11-28 15:05:49 +01:00
Dennis Guse f138826d60 Text in charts now scales using Android's global settings.
Fixes #524.
2020-11-28 15:05:49 +01:00
Dennis Guse 14ac29095d Removed unused theme. 2020-11-28 15:05:49 +01:00
Dennis Guse a3f4de7d18 Interval list: table header in accent color. 2020-11-28 15:05:49 +01:00
Dennis Guse 6392e88dc7 Enable RTL. 2020-11-28 15:05:49 +01:00
FTno 1e913d6bb5 Update strings.xml
Typos in source strings - Issue #530
2020-11-26 07:35:34 +01:00
dependabot[bot] ced8454f59 Bump mockito-android from 3.6.0 to 3.6.28
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.6.0 to 3.6.28.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.6.0...v3.6.28)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-25 12:15:57 +01:00
Román Martínez 974a5afeb1 Only clean if parse error is out of <trk> 2020-11-25 06:39:07 +01:00
Román Martínez 9c9bdde6b9 Import files by track in memory (avoid clean track when error). Fixes #473. 2020-11-25 06:39:07 +01:00
Dennis Guse ca2bc6426c Release: v3.13.2 2020-11-22 20:21:50 +01:00
Dennis Guse 81097aac40 Bugfix: crash in landscape mode in TrackController due to wrong background. 2020-11-22 20:21:11 +01:00
Dennis Guse a74b5fc743 Cleanup. 2020-11-21 18:42:32 +01:00
Dennis Guse e996ad0a0c Release: v3.13.1 2020-11-21 10:37:25 +01:00
Dennis Guse 0f726958e0 Revert "Shrinking all APK builds without obfuscation. Fixes #433."
Introduced #523.

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-04 07:46:08 +01:00
115 changed files with 1979 additions and 2887 deletions
-10
View File
@@ -1,10 +0,0 @@
[main]
host = https://www.transifex.com
[opentracks.src-main-res-values-strings-xml--main]
file_filter = src/main/res/values-<lang>/strings.xml
minimum_perc = 0
source_file = src/main/res/values/strings.xml
source_lang = en
type = ANDROID
+6 -3
View File
@@ -2,6 +2,8 @@
_OpenTracks_ is a sport tracking application that completely respects your privacy.
[![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech)
<table>
<tr>
<th>Free</th>
@@ -20,8 +22,8 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
</a>
</td>
<td align="center">
<a href="https://www.transifex.com/opentracks/opentracks/">
Transifex
<a href="https://weblate.bubu1.eu/engage/OpenTracks/">
<img src="https://weblate.bubu1.eu/widgets/OpenTracks/-/opentracks/svg-badge.svg" alt="Translation status" />
</a>
</td>
</tr>
@@ -53,6 +55,7 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
* __Bluetooth LE sensors:__
* heart rate
* cycling: speed and cadence
* cycling: power meter
* __Export data:__ export tracks either as KMZ (incl. photos), KML or GPX
* __No Internet access:__ Internet is not used
* __Voice announcements__
@@ -68,7 +71,7 @@ __Only required permissions:__
An overview of Bluetooth LE sensors that are known to work with OpenTracks is in [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md).
_Please note:_
_OpenTracks_ does not contain a _map_ (as this would required Internet access).
_OpenTracks_ does not contain a _map_ (as this would require internet access).
## Custom Dashboards (incl. Map)
As of v3.3.1 OpenTracks enables to use custom dashboards.
+1 -1
View File
@@ -34,4 +34,4 @@ Requirement: `gem install fastlane`
## Translations (localization)
Translations of the OpenTracks are handled in Transifex: [https://www.transifex.com/opentracks](https://www.transifex.com/opentracks/).
Translations of the OpenTracks are handled in Weblate: [https://weblate.bubu1.eu/projects/OpenTracks/](https://weblate.bubu1.eu/projects/OpenTracks/).
+25 -2
View File
@@ -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
View File
@@ -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
View File
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.android.tools.build:gradle:4.1.1'
}
}
@@ -51,8 +51,8 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 3704
versionName "v3.12.0"
versionCode 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
View File
@@ -10,15 +10,11 @@ endlegend
[*] --> TrackListActivity
TrackListActivity --> TrackRecordingActivity : Start recording
TrackListActivity --> SearchListActivity
TrackListActivity --> TrackRecordedActivity : Show track
TrackListActivity --> MarkerListActivity : Show all markers
TrackListActivity --> AggregatedStatisticsActivity : Aggregated stats
TrackListActivity --> TrackRecordedActivity : Show track
TrackListActivity --> TrackDeleteActivity
SearchListActivity --> MarkerDetailActivity : Search result
SearchListActivity --> TrackRecordedActivity : Search result
TrackRecordedActivity --> TrackDeleteActivity
TrackRecordedActivity --> TrackEditActivity
TrackRecordedActivity --> MarkerListActivity
@@ -46,11 +42,14 @@ MarkerDetailActivity --> CoordinateOnMapIntent : URI
SettingsActivity --> AboutActivity
SettingsActivity --> HelpActivity
SettingsActivity --> ImportActivity
SettingsActivity --> ExportActivity
SettingsActivity --> DirectoryChooserActivity$ImportDirectoryChooserActivity
SettingsActivity --> DirectoryChooserActivity$ExportDirectoryChooserActivity
SettingsActivity --> DirectoryChooserActivity$DefaultTrackExportDirectoryChooserActivity
DirectoryChooserActivity$ImportDirectoryChooserActivity --> ImportActivity
DirectoryChooserActivity$ExportDirectoryChooserActivity --> ExportActivity
SettingsActivity --> DirectoryChooserActivity.ImportDirectoryChooserActivity
SettingsActivity --> DirectoryChooserActivity.ExportDirectoryChooserActivity
SettingsActivity --> DirectoryChooserActivity.DefaultTrackExportDirectoryChooserActivity
DirectoryChooserActivity.ImportDirectoryChooserActivity --> ImportActivity
DirectoryChooserActivity.ExportDirectoryChooserActivity --> ExportActivity
FileImportIntent --> ImportActivity
@enduml
@@ -5,6 +5,6 @@ Funktionen & Features:
* zeichnet Sport und andere Outdooraktivitäten auf
* unterstützt das Markieren interessanter Orte, inkl. Foto-Funktion
* zeichnet deine Herzfrequenz auf (Bluetooth LE)
* nutze beim Radfahren deine Trittfrequenz- und Geschwindigkeitssensoren (Bluetooth LE)
* nutze beim Radfahren deine Trittfrequenz- und Geschwindigkeitssensoren sowie Leistungsmesser (Bluetooth LE)
* exportiert deine Daten als KML oder GPX
* funktioniert ganz ohne Internet
@@ -0,0 +1,10 @@
v3.13.0: OpenTracks
Changes:
- Sensor: support for powermeter (BLE)
- Search by track and marker (in lists)
- Combined search removed
Bugfix:
- Import crashed in UI
- Images of markers were not imported
@@ -0,0 +1,5 @@
v3.13.1: OpenTracks
Bugfix:
- Crash on record start: minify/shrink removed a drawable
- Automatic export failed silently (mainly remote destinations)
@@ -0,0 +1,5 @@
v3.13.2: OpenTracks
Bugfix:
- Crash in landscape mode
@@ -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());
}
}
}
@@ -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() {
@@ -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);
}
}
@@ -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));
}
}
+173
View File
@@ -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>
+67 -58
View File
@@ -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,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,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;
@@ -32,6 +32,7 @@ import java.util.UUID;
import de.dennisguse.opentracks.content.sensor.SensorData;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import de.dennisguse.opentracks.content.sensor.SensorDataCyclingPower;
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
import de.dennisguse.opentracks.util.BluetoothUtils;
@@ -88,8 +89,14 @@ public abstract class BluetoothConnectionManager {
// Register for updates.
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(BluetoothUtils.CLIENT_CHARACTERISTIC_CONFIG_UUID);
if (descriptor == null) {
Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID characteristic not available; cannot request notifications for changed data.");
return;
}
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);
}
@Override
@@ -170,7 +177,7 @@ public abstract class BluetoothConnectionManager {
public static class CyclingCadence extends BluetoothConnectionManager {
CyclingCadence(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
}
@Override
@@ -201,7 +208,7 @@ public abstract class BluetoothConnectionManager {
public static class CyclingSpeed extends BluetoothConnectionManager {
CyclingSpeed(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
}
@Override
@@ -219,6 +226,25 @@ public abstract class BluetoothConnectionManager {
}
}
public static class CyclingPower extends BluetoothConnectionManager {
CyclingPower(@NonNull SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_POWER_UUID, BluetoothUtils.CYCLING_POWER_MEASUREMENT_CHAR_UUID, observer);
}
@Override
protected SensorData createEmptySensorData(String address) {
return new SensorDataCyclingPower(address);
}
@Override
protected SensorDataCyclingPower parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
Integer cyclingPower = BluetoothUtils.parseCyclingPower(characteristic);
return cyclingPower != null ? new SensorDataCyclingPower(address, sensorName, cyclingPower) : null;
}
}
interface SensorDataObserver {
void onChanged(SensorData sensorData);
@@ -59,6 +59,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
private final BluetoothConnectionManager.HeartRate heartRate = new BluetoothConnectionManager.HeartRate(this);
private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
private final BluetoothConnectionManager.CyclingSpeed cyclingSpeed = new BluetoothConnectionManager.CyclingSpeed(this);
private final BluetoothConnectionManager.CyclingPower cyclingPower = new BluetoothConnectionManager.CyclingPower(this);
private final SensorDataSet sensorDataSet = new SensorDataSet();
@@ -82,6 +83,12 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
connect(cyclingSpeed, address);
}
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_power_key, key)) {
String address = PreferencesUtils.getBluetoothCyclingPowerSensorAddress(context);
connect(cyclingPower, address);
}
}
};
@@ -102,6 +109,8 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
public synchronized void stop() {
heartRate.disconnect();
cyclingCadence.disconnect();
cyclingSpeed.disconnect();
cyclingPower.disconnect();
sensorDataSet.clear();
@@ -19,12 +19,10 @@ package de.dennisguse.opentracks.settings;
import android.content.Context;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Spinner;
import android.widget.ImageView;
import androidx.preference.DialogPreference;
import androidx.preference.PreferenceDialogFragmentCompat;
@@ -63,7 +61,7 @@ public class ActivityTypePreference extends DialogPreference {
public static class ActivityPreferenceDialog extends PreferenceDialogFragmentCompat {
private AutoCompleteTextView textView;
private Spinner spinner;
private ImageView iconView;
static ActivityPreferenceDialog newInstance(String preferenceKey) {
ActivityTypePreference.ActivityPreferenceDialog dialog = new ActivityTypePreference.ActivityPreferenceDialog();
@@ -87,30 +85,19 @@ public class ActivityTypePreference extends DialogPreference {
textView.setAdapter(adapter);
textView.setOnItemClickListener((parent, v, position, id) -> {
String iconValue = TrackIconUtils.getIconValue(context, (String) textView.getAdapter().getItem(position));
TrackIconUtils.setIconSpinner(spinner, iconValue);
updateIcon(iconValue);
});
textView.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
String iconValue = TrackIconUtils.getIconValue(context, textView.getText().toString());
TrackIconUtils.setIconSpinner(spinner, iconValue);
updateIcon(iconValue);
}
});
String iconValue = TrackIconUtils.getIconValue(context, category);
spinner = view.findViewById(R.id.activity_type_preference_spinner);
spinner.setAdapter(TrackIconUtils.getIconSpinnerAdapter(context, iconValue));
spinner.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
showIconSelectDialog();
}
return true;
});
spinner.setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
showIconSelectDialog();
}
return true;
});
iconView = view.findViewById(R.id.activity_type_preference_spinner);
iconView.setOnClickListener((it) -> showIconSelectDialog());
updateIcon(TrackIconUtils.getIconValue(context, category));
}
private void showIconSelectDialog() {
@@ -129,15 +116,13 @@ public class ActivityTypePreference extends DialogPreference {
}
}
/**
* Updates the value of the dialog.
*
* @param iconValue the icon value
*/
public void updateUI(String iconValue) {
TrackIconUtils.setIconSpinner(spinner, iconValue);
textView.setText(getActivity().getString(TrackIconUtils.getIconActivityType(iconValue)));
textView.clearFocus();
}
private void updateIcon(String iconValue) {
iconView.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
}
}
}
@@ -1,10 +1,12 @@
package de.dennisguse.opentracks.settings;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.documentfile.provider.DocumentFile;
@@ -20,6 +22,7 @@ import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingPowerPreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeSensorPreference;
import de.dennisguse.opentracks.util.ActivityUtils;
@@ -31,8 +34,10 @@ import de.dennisguse.opentracks.util.StringUtils;
public class SettingsActivity extends AppCompatActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, ResetDialogPreference.ResetCallback {
private static final String TAG = SettingsActivity.class.getSimpleName();
public static final String EXTRAS_CHECK_EXPORT_DIRECTORY = "Check Export Directory";
private PrefsFragment prefsFragment;
private boolean checkExportDirectory = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -44,9 +49,29 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
toolbar.setTitle(R.string.menu_settings);
setSupportActionBar(toolbar);
Intent intent = getIntent();
if (intent != null && intent.hasExtra(EXTRAS_CHECK_EXPORT_DIRECTORY)) {
checkExportDirectory = true;
}
onReset();
}
@Override
protected void onResume() {
if (checkExportDirectory) {
checkExportDirectory = false;
new AlertDialog.Builder(this)
.setIcon(R.drawable.ic_logo_24dp)
.setTitle(R.string.app_name)
.setMessage(R.string.export_error_post_workout)
.setNeutralButton(R.string.generic_ok, null)
.create()
.show();
}
super.onResume();
}
@Override
public void onChooseActivityTypeDone(String iconValue) {
prefsFragment.setDefaultActivity(iconValue);
@@ -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) {
@@ -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);
}
}
@@ -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" />
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_button_stop_disabled" android:state_enabled="false" />
<item android:drawable="@drawable/ic_button_stop_pressed" android:state_pressed="true" />
<item android:drawable="@drawable/ic_button_stop" />
</selector>
@@ -24,9 +24,4 @@
android:strokeWidth="4.82273865"
android:strokeAlpha="1"
android:strokeColor="#00000000" />
<path
android:name="fade_out_circle"
android:fillAlpha="0.0"
android:fillColor="#a31a1f"
android:pathData="M62.5,62.5m-0,-62a50,50 0,1 1,-0 123a50,50 0,1 1,-0 -123" />
</vector>
@@ -1,6 +0,0 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_button_pause">
<target
android:name="fade_out_circle"
android:animation="@animator/button_animator" />
</animated-vector>
@@ -1,41 +0,0 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:width="82dp"
android:height="82dp"
android:viewportWidth="125"
android:viewportHeight="125">
<path
android:fillAlpha="1"
android:fillColor="#00000000"
android:pathData="M2.516,63.008a60.239,59.73 134.879,1 0,119.968 -1.017a60.239,59.73 134.879,1 0,-119.968 1.017z"
android:strokeWidth="5.03194237"
android:strokeAlpha="1"
android:strokeColor="#555555" />
<path
android:fillAlpha="1"
android:fillColor="#484848"
android:pathData="M62.5,62.5m-50,0a50,50 0,1 1,100 0a50,50 0,1 1,-100 0"
android:strokeWidth="6.9172821"
android:strokeAlpha="1"
android:strokeColor="#00000000" />
<path
android:fillAlpha="1"
android:fillColor="#ffffff"
android:fillType="nonZero"
android:pathData="M53.02,54.02h17.46v17.46h-17.46z"
android:strokeWidth="1.04023623"
android:strokeAlpha="1"
android:strokeColor="#414141" />
<path
android:name="fade_out_circle"
android:fillAlpha="0.0"
android:fillColor="#a31a1f"
android:pathData="M62.5,62.5m-0,-62a50,50 0,1 1,-0 123a50,50 0,1 1,-0 -123"></path>
</vector>
</aapt:attr>
<target
android:name="fade_out_circle"
android:animation="@animator/button_animator" />
</animated-vector>
@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="82dp"
android:height="82dp"
android:viewportWidth="125"
android:viewportHeight="125">
<path
android:fillColor="#00000000"
android:pathData="M2.516,63.009a60.239,59.73 134.856,1 0,119.967 -1.017a60.239,59.73 134.856,1 0,-119.967 1.017z"
android:strokeWidth="5.0319"
android:strokeColor="#555" />
<path android:pathData="M62.5,62.5m-0,-50a50,50 0,1 1,-0 100a50,50 0,1 1,-0 -100">
<aapt:attr name="android:fillColor">
<gradient
android:endX="62.5"
android:endY="112.5"
android:startX="62.5"
android:startY="12.5"
android:type="linear">
<item
android:color="#FF404040"
android:offset="0" />
<item
android:color="#FF555555"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#dbdbdb"
android:pathData="M53.02,54.02h17.46v17.46h-17.46z"
android:strokeWidth="1.0402"
android:strokeColor="#414141" />
</vector>
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
</vector>
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z" />
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<transition xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The drawables used here can be solid colors, gradients, shapes, images, etc. -->
<item android:drawable="@color/track_controller_background_normal" />
<item android:drawable="@color/track_controller_background_activated" />
</transition>
@@ -19,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>
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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"
+4 -7
View File
@@ -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>
+6 -6
View File
@@ -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"/>
+4 -4
View File
@@ -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"/>
+18 -34
View File
@@ -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>
+6 -5
View File
@@ -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"
+2 -2
View File
@@ -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"
-32
View File
@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2012 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar" />
<ListView
android:id="@+id/search_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/search_list_empty"
style="@style/ListEmptyTextView"
android:text="@string/search_list_empty_message" />
</LinearLayout>
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:id="@+id/stats_sensor_label"
style="@style/StatsLargeLabel"
tools:text="@string/sensor_state_heart_rate"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/stats_sensor_sensor_value"
style="@style/StatsTinyValue"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/stats_sensor_label"
tools:text="90" />
<TextView
android:id="@+id/stats_sensor_value"
style="@style/StatsLargeValue"
android:value="@string/value_unknown"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/stats_sensor_sensor_value"
tools:text="90" />
<TextView
android:id="@+id/stats_sensor_unit"
style="@style/StatsUnit"
tools:text="@string/sensor_unit_beats_per_minute"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/stats_sensor_value" />
</androidx.constraintlayout.widget.ConstraintLayout>
+48 -48
View File
@@ -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