Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d16cbe04a5 | |||
| 90a8fc898a | |||
| 8d141b1324 | |||
| 43e0f33776 | |||
| e4270b983a | |||
| 0209a90e01 | |||
| 0f92a37146 | |||
| 9cebb1013c | |||
| 6ac0db7238 | |||
| 3c1ab01df6 | |||
| b6850eb46f | |||
| 08b9cb31fa | |||
| 9541a92657 | |||
| acc2627bde | |||
| 48c3695b5d | |||
| 2b280f050c | |||
| 68f34d9682 | |||
| f56820e132 | |||
| 0f5d72f098 | |||
| accbc5a8ac | |||
| 0bc9f6a042 | |||
| bbb91fedc5 | |||
| 433ab01ece | |||
| 88e1a95da7 | |||
| d4483819eb | |||
| 917b8f4056 | |||
| 70c1379778 | |||
| 5a60f40308 | |||
| df9589697a | |||
| 27443cc716 | |||
| 654be751a7 | |||
| 3a8f3e46c7 | |||
| b270c45691 | |||
| 257d8c1276 | |||
| 8521b98f4b | |||
| eaf655f6e7 | |||
| 24c7f3fd7a | |||
| a4bab656e4 | |||
| 679c1d7e3e | |||
| 0036934fb3 | |||
| c508747203 | |||
| 0f4c0079d7 | |||
| 9e6bb1c511 |
@@ -1,5 +1,26 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
|
<JetCodeStyleSettings>
|
||||||
|
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||||
|
<value>
|
||||||
|
<package name="java.util" alias="false" withSubpackages="false" />
|
||||||
|
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||||
|
<package name="io.ktor" alias="false" withSubpackages="true" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="PACKAGES_IMPORT_LAYOUT">
|
||||||
|
<value>
|
||||||
|
<package name="" alias="false" withSubpackages="true" />
|
||||||
|
<package name="java" alias="false" withSubpackages="true" />
|
||||||
|
<package name="javax" alias="false" withSubpackages="true" />
|
||||||
|
<package name="kotlin" alias="false" withSubpackages="true" />
|
||||||
|
<package name="" alias="true" withSubpackages="true" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</JetCodeStyleSettings>
|
||||||
|
<MarkdownNavigatorCodeStyleSettings>
|
||||||
|
<option name="RIGHT_MARGIN" value="72" />
|
||||||
|
</MarkdownNavigatorCodeStyleSettings>
|
||||||
<codeStyleSettings language="XML">
|
<codeStyleSettings language="XML">
|
||||||
<indentOptions>
|
<indentOptions>
|
||||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
@@ -5,34 +5,46 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
|
|||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Free</th>
|
<th>Free</th>
|
||||||
<th>Paid (same as free)</th>
|
|
||||||
<th>Donations</th>
|
<th>Donations</th>
|
||||||
<th>Translations</th>
|
<th>Translations</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td align="center">
|
||||||
<a href="https://f-droid.org/packages/de.dennisguse.opentracks">
|
<a href="https://f-droid.org/packages/de.dennisguse.opentracks">
|
||||||
<img alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="60" align="middle">
|
<img alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="60" align="middle">
|
||||||
<br />
|
|
||||||
<img alt="OpenTracks version published on F-Droid" src="https://img.shields.io/f-droid/v/de.dennisguse.opentracks.svg" align="middle" >
|
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td align="center">
|
||||||
<a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">
|
|
||||||
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" height="60" align="middle">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="https://liberapay.com/OpenTracks/donate">
|
<a href="https://liberapay.com/OpenTracks/donate">
|
||||||
<img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" height="45" align="middle">
|
<img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" height="45" align="middle">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td align="center">
|
||||||
<a href="https://www.transifex.com/opentracks/opentracks/">
|
<a href="https://www.transifex.com/opentracks/opentracks/">
|
||||||
Transifex
|
Transifex
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<img alt="OpenTracks version published on F-Droid" src="https://img.shields.io/f-droid/v/de.dennisguse.opentracks.svg" align="middle" >
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="bitcoin:bc1qckpls3lpjlcpyxzm66hcy65hnesas9zgacy6yc">
|
||||||
|
<img alt="bitcoin:bc1qckpls3lpjlcpyxzm66hcy65hnesas9zgacy6yc" src="promotion/donations-bitcoin.png" height="90" align="middle">
|
||||||
|
</a>
|
||||||
|
</td align="center">
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">
|
||||||
|
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" height="60" align="middle">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## Features:
|
## Features:
|
||||||
@@ -69,12 +81,18 @@ However, this is rather slow and does not provide updates while recording.
|
|||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
<div>
|
<div>
|
||||||
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_1.png">
|
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_1.png">
|
||||||
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_2.png">
|
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_2.png">
|
||||||
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_3.png">
|
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_3.png">
|
||||||
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_4.png">
|
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_4.png">
|
||||||
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_5.png">
|
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_5.png">
|
||||||
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_6.png">
|
</div>
|
||||||
|
<div>
|
||||||
|
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_6.png">
|
||||||
|
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_7.png">
|
||||||
|
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_8.png">
|
||||||
|
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_9.png">
|
||||||
|
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_10.png">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Project history
|
## Project history
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## HRS: Heartrate Service
|
## HRS: Heartrate Service
|
||||||
* Decathlon|Geonaute|Kalenji Dual HRM Belt
|
* Decathlon|Geonaute|Kalenji Dual HRM Belt
|
||||||
|
* moofit heart rate monitor
|
||||||
* Polar H7
|
* Polar H7
|
||||||
* Polar OH1
|
* Polar OH1
|
||||||
Has updatable firmware that requires a user account at polar.com
|
Has updatable firmware that requires a user account at polar.com
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.0.1'
|
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,10 +45,14 @@ android {
|
|||||||
|
|
||||||
compileSdkVersion 30
|
compileSdkVersion 30
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "de.dennisguse.opentracks"
|
applicationId "de.dennisguse.opentracks"
|
||||||
versionCode 3661
|
versionCode 3704
|
||||||
versionName "v3.11.1"
|
versionName "v3.12.0"
|
||||||
|
|
||||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||||
|
|
||||||
@@ -88,11 +92,11 @@ dependencies {
|
|||||||
implementation 'androidx.preference:preference:1.1.1'
|
implementation 'androidx.preference:preference:1.1.1'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'com.google.android.material:material:1.2.1'
|
implementation 'com.google.android.material:material:1.2.1'
|
||||||
implementation 'com.android.support.constraint:constraint-layout:2.0.1'
|
implementation 'com.android.support.constraint:constraint-layout:2.0.4'
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test:core:1.3.0'
|
androidTestImplementation 'androidx.test:core:1.3.0'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||||
androidTestImplementation 'org.mockito:mockito-android:3.5.13'
|
androidTestImplementation 'org.mockito:mockito-android:3.6.0'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ MarkerDetailActivity --> CoordinateOnMapIntent : URI
|
|||||||
SettingsActivity --> AboutActivity
|
SettingsActivity --> AboutActivity
|
||||||
SettingsActivity --> HelpActivity
|
SettingsActivity --> HelpActivity
|
||||||
SettingsActivity --> ExportActivity
|
SettingsActivity --> ExportActivity
|
||||||
SettingsActivity --> DirectoryChooserActivity
|
SettingsActivity --> DirectoryChooserActivity$ImportDirectoryChooserActivity
|
||||||
SettingsActivity --> DirectoryChooserActivity
|
SettingsActivity --> DirectoryChooserActivity$ExportDirectoryChooserActivity
|
||||||
|
SettingsActivity --> DirectoryChooserActivity$DefaultTrackExportDirectoryChooserActivity
|
||||||
DirectoryChooserActivity$ImportDirectoryChooserActivity --> ImportActivity
|
DirectoryChooserActivity$ImportDirectoryChooserActivity --> ImportActivity
|
||||||
DirectoryChooserActivity$ExportDirectoryChooserActivity --> ExportActivity
|
DirectoryChooserActivity$ExportDirectoryChooserActivity --> ExportActivity
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ LocationManager --> TrackRecordingService
|
|||||||
ElevationSumManager --> TrackRecordingService
|
ElevationSumManager --> TrackRecordingService
|
||||||
|
|
||||||
TrackRecordingService --> TrackRecordingServiceConnection
|
TrackRecordingService --> TrackRecordingServiceConnection
|
||||||
TrackRecordingService --> Database
|
TrackRecordingService --> CustomContentProvider
|
||||||
|
CustomContentProvider --> Database
|
||||||
|
|
||||||
Database --> CustomContentProvider
|
Database --> CustomContentProvider
|
||||||
CustomContentProvider --> TrackDataHub
|
CustomContentProvider --> TrackDataHub
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class Track {
|
|||||||
-icon
|
-icon
|
||||||
}
|
}
|
||||||
Track o-- TrackPoint
|
Track o-- TrackPoint
|
||||||
Track o-- Waypoint
|
Track o-- Marker
|
||||||
Track "1" - "1" TripStatistics
|
Track "1" - "1" TripStatistics
|
||||||
|
|
||||||
class TripStatistics {
|
class TripStatistics {
|
||||||
@@ -21,7 +21,12 @@ class TripStatistics {
|
|||||||
- totalElevationGain
|
- totalElevationGain
|
||||||
}
|
}
|
||||||
|
|
||||||
class TrackPoint extends android.location.Location {
|
note left of TripStatistics
|
||||||
|
The time is based upon device time.
|
||||||
|
end note
|
||||||
|
|
||||||
|
|
||||||
|
class TrackPoint {
|
||||||
- id
|
- id
|
||||||
- trackId
|
- trackId
|
||||||
- longitude
|
- longitude
|
||||||
@@ -36,7 +41,32 @@ class TrackPoint extends android.location.Location {
|
|||||||
- sensor_power
|
- sensor_power
|
||||||
}
|
}
|
||||||
|
|
||||||
class Waypoint {
|
note right of TrackPoint
|
||||||
|
TODO
|
||||||
|
TrackPoints are mixing two concepts:
|
||||||
|
1. a GPS measurement provided by the GPS hardware
|
||||||
|
2. and also segment markers.
|
||||||
|
These are marked by PAUSE / RESUME TrackPoints.
|
||||||
|
|
||||||
|
While exporting PAUSE/RESUME are not used to create these segments and these are partly recreated when imported.
|
||||||
|
However, the timestamp of PAUSE/RESUME is not exported and can therefore not be restored.
|
||||||
|
This prevents us from restoring the exact TripStatistics.
|
||||||
|
It is also noteworthy that neither PAUSE/RESUME TrackPoints are used for computing the TrackStatistics.
|
||||||
|
Only the timestamp to _create_ those is used.
|
||||||
|
end note
|
||||||
|
|
||||||
|
note left of TrackPoint
|
||||||
|
TODO
|
||||||
|
The time stored in TrackPoints is either:
|
||||||
|
1. created via GPS: provided by the GPS hardware
|
||||||
|
2. created by the user (pause/resume): device time.
|
||||||
|
|
||||||
|
We know that the GPS time and device time are often not in sync.
|
||||||
|
end note
|
||||||
|
|
||||||
|
TrackPoint "1" - "1" android.location.Location
|
||||||
|
|
||||||
|
class Marker {
|
||||||
- id
|
- id
|
||||||
- trackId
|
- trackId
|
||||||
- name
|
- name
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
v3.12.0: OpenTracks
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
- Bluetooth sensor addresses
|
||||||
|
- Intervals: show elevation gain
|
||||||
|
- Export: default export directory
|
||||||
|
- Export: automatic export after finishing a track
|
||||||
|
- Manual import/export: new user interface
|
||||||
|
- Manual export: option to overwrite on manual export
|
||||||
|
|
||||||
|
Faster:
|
||||||
|
- Track lists loading
|
||||||
|
- Deleting multiple tracks
|
||||||
|
|
||||||
|
Bugfix:
|
||||||
|
- Fixed intent filters for import
|
||||||
|
- KMZ could not be parsed
|
||||||
|
- Export: fix crash on Android 11
|
||||||
|
|
||||||
|
Developer:
|
||||||
|
- Use ViewBindings
|
||||||
|
- Database enforces foreign key and uses cascading deletes
|
||||||
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 144 KiB |
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ esac
|
|||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
@@ -130,7 +129,6 @@ fi
|
|||||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
# We build the pattern for arguments to be converted via cygpath
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ set CMD_LINE_ARGS=%*
|
|||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -109,26 +109,21 @@ public class SearchEngineTest {
|
|||||||
if (hoursAgo >= 0) {
|
if (hoursAgo >= 0) {
|
||||||
location.setTime(NOW - hoursAgo * 1000L * 60L * 60L);
|
location.setTime(NOW - hoursAgo * 1000L * 60L * 60L);
|
||||||
}
|
}
|
||||||
Marker waypoint = new Marker(location);
|
Marker waypoint = new Marker(trackId, location);
|
||||||
waypoint.setName(title);
|
waypoint.setName(title);
|
||||||
waypoint.setDescription(description);
|
waypoint.setDescription(description);
|
||||||
waypoint.setCategory(category);
|
waypoint.setCategory(category);
|
||||||
waypoint.setTrackId(trackId);
|
|
||||||
|
|
||||||
Uri uri = providerUtils.insertMarker(waypoint);
|
Uri uri = providerUtils.insertMarker(waypoint);
|
||||||
return new Marker.Id(ContentUris.parseId(uri));
|
return new Marker.Id(ContentUris.parseId(uri));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Marker.Id insertWaypoint(String title, String description, String category) {
|
private Marker.Id insertWaypoint(String title, String description, String category, Track.Id trackId) {
|
||||||
return insertWaypoint(title, description, category, 0.0, -1, null);
|
return insertWaypoint(title, description, category, 0.0, -1, trackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Marker.Id insertWaypoint(String title, double distance) {
|
private Marker.Id insertWaypoint(String title, double distance, Track.Id trackId) {
|
||||||
return insertWaypoint(title, "", "", distance, -1, null);
|
return insertWaypoint(title, "", "", distance, -1, trackId);
|
||||||
}
|
|
||||||
|
|
||||||
private Marker.Id insertWaypoint(String title, long hoursAgo) {
|
|
||||||
return insertWaypoint(title, "", "", 0.0, hoursAgo, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Marker.Id insertWaypoint(String title, long hoursAgo, Track.Id trackId) {
|
private Marker.Id insertWaypoint(String title, long hoursAgo, Track.Id trackId) {
|
||||||
@@ -170,13 +165,14 @@ public class SearchEngineTest {
|
|||||||
// - one which will match in title and category
|
// - one which will match in title and category
|
||||||
// - one which will match in title and description
|
// - one which will match in title and description
|
||||||
// - one which will match in all fields
|
// - one which will match in all fields
|
||||||
insertWaypoint("bb", "cc", "dd");
|
Track.Id trackId = insertTrack("", "", "");
|
||||||
Marker.Id descriptionMatchId = insertWaypoint("bb", "aa", "cc");
|
insertWaypoint("bb", "cc", "dd", trackId);
|
||||||
Marker.Id categoryMatchId = insertWaypoint("bb", "cc", "aa");
|
Marker.Id descriptionMatchId = insertWaypoint("bb", "aa", "cc", trackId);
|
||||||
Marker.Id titleMatchId = insertWaypoint("aa", "bb", "cc");
|
Marker.Id categoryMatchId = insertWaypoint("bb", "cc", "aa", trackId);
|
||||||
Marker.Id titleCategoryMatchId = insertWaypoint("aa", "bb", "ca");
|
Marker.Id titleMatchId = insertWaypoint("aa", "bb", "cc", trackId);
|
||||||
Marker.Id titleDescriptionMatchId = insertWaypoint("aa", "ba", "cc");
|
Marker.Id titleCategoryMatchId = insertWaypoint("aa", "bb", "ca", trackId);
|
||||||
Marker.Id allMatchId = insertWaypoint("aa", "ba", "ca");
|
Marker.Id titleDescriptionMatchId = insertWaypoint("aa", "ba", "cc", trackId);
|
||||||
|
Marker.Id allMatchId = insertWaypoint("aa", "ba", "ca", trackId);
|
||||||
|
|
||||||
SearchQuery query = new SearchQuery("a", null, null, NOW);
|
SearchQuery query = new SearchQuery("a", null, null, NOW);
|
||||||
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
|
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
|
||||||
@@ -193,11 +189,11 @@ public class SearchEngineTest {
|
|||||||
// - one waypoint which will match by title
|
// - one waypoint which will match by title
|
||||||
// - one track which won't match
|
// - one track which won't match
|
||||||
// - one track which will match by title
|
// - one track which will match by title
|
||||||
Marker.Id descriptionWaypointId = insertWaypoint("bb", "aa", "cc");
|
|
||||||
insertWaypoint("bb", "cc", "dd");
|
|
||||||
Marker.Id titleWaypointId = insertWaypoint("aa", "bb", "cc");
|
|
||||||
insertTrack("bb", "cc", "dd");
|
|
||||||
Track.Id trackId = insertTrack("aa", "bb", "cc");
|
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);
|
SearchQuery query = new SearchQuery("a", null, null, NOW);
|
||||||
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
|
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
|
||||||
@@ -212,9 +208,10 @@ public class SearchEngineTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testSearchWaypointDistance() {
|
public void testSearchWaypointDistance() {
|
||||||
// All results match text, but they're at difference distances from the user.
|
// All results match text, but they're at difference distances from the user.
|
||||||
Marker.Id farFarAwayId = insertWaypoint("aa", 0.3);
|
Track.Id trackId = insertTrack("", "", "");
|
||||||
Marker.Id nearId = insertWaypoint("ab", 0.1);
|
Marker.Id farFarAwayId = insertWaypoint("aa", 0.3, trackId);
|
||||||
Marker.Id farId = insertWaypoint("ac", 0.2);
|
Marker.Id nearId = insertWaypoint("ab", 0.1, trackId);
|
||||||
|
Marker.Id farId = insertWaypoint("ac", 0.2, trackId);
|
||||||
|
|
||||||
SearchQuery query = new SearchQuery("a", HERE, null, NOW);
|
SearchQuery query = new SearchQuery("a", HERE, null, NOW);
|
||||||
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
|
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
|
||||||
@@ -239,9 +236,10 @@ public class SearchEngineTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testSearchWaypointRecent() {
|
public void testSearchWaypointRecent() {
|
||||||
// All results match text, but they're were recorded at different times.
|
// All results match text, but they're were recorded at different times.
|
||||||
Marker.Id oldestId = insertWaypoint("aa", 2);
|
Track.Id trackId = insertTrack("", "", "");
|
||||||
Marker.Id recentId = insertWaypoint("ab", 0);
|
Marker.Id oldestId = insertWaypoint("aa", 2, trackId);
|
||||||
Marker.Id oldId = insertWaypoint("ac", 1);
|
Marker.Id recentId = insertWaypoint("ab", 0, trackId);
|
||||||
|
Marker.Id oldId = insertWaypoint("ac", 1, trackId);
|
||||||
|
|
||||||
SearchQuery query = new SearchQuery("a", null, null, NOW);
|
SearchQuery query = new SearchQuery("a", null, null, NOW);
|
||||||
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
|
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
|
||||||
@@ -266,10 +264,13 @@ public class SearchEngineTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testSearchCurrentTrackWaypoint() {
|
public void testSearchCurrentTrackWaypoint() {
|
||||||
// All results match text, but one of them is in the current track.
|
// All results match text, but one of them is in the current track.
|
||||||
Marker.Id otherId = insertWaypoint("aa", 1, new Track.Id(456));
|
Track.Id trackId1 = insertTrack("", "", "");
|
||||||
Marker.Id currentId = insertWaypoint("ab", 1, new Track.Id(123));
|
Track.Id trackId2 = insertTrack("", "", "");
|
||||||
|
|
||||||
SearchQuery query = new SearchQuery("a", null, new Track.Id(123), NOW);
|
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));
|
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
|
||||||
|
|
||||||
// Waypoint in current track should be promoted.
|
// Waypoint in current track should be promoted.
|
||||||
@@ -316,7 +317,7 @@ public class SearchEngineTest {
|
|||||||
ScoredResult result = results.get(i);
|
ScoredResult result = results.get(i);
|
||||||
assertNotNull(errMsg, result.marker);
|
assertNotNull(errMsg, result.marker);
|
||||||
assertNull(errMsg, result.track);
|
assertNull(errMsg, result.track);
|
||||||
assertEquals(errMsg, waypointIds[i], result.marker.getId());
|
assertEquals(errMsg, waypointIds[i].getId(), result.marker.getId().getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public class TestDataUtil {
|
|||||||
public static final double INITIAL_LATITUDE = 37.0;
|
public static final double INITIAL_LATITUDE = 37.0;
|
||||||
public static final double INITIAL_LONGITUDE = -57.0;
|
public static final double INITIAL_LONGITUDE = -57.0;
|
||||||
public static final double ALTITUDE_INTERVAL = 2.5;
|
public static final double ALTITUDE_INTERVAL = 2.5;
|
||||||
|
public static final float ELEVATION_GAIN = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a track without any trackPoints.
|
* Create a track without any trackPoints.
|
||||||
@@ -70,7 +71,7 @@ public class TestDataUtil {
|
|||||||
trackPoint.setHeartRate_bpm(100f + i);
|
trackPoint.setHeartRate_bpm(100f + i);
|
||||||
trackPoint.setCyclingCadence_rpm(300f + i);
|
trackPoint.setCyclingCadence_rpm(300f + i);
|
||||||
trackPoint.setPower(400f + i);
|
trackPoint.setPower(400f + i);
|
||||||
trackPoint.setElevationGain(500f + i);
|
trackPoint.setElevationGain(ELEVATION_GAIN);
|
||||||
return trackPoint;
|
return trackPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ public class CustomContentProviderUtilsTest {
|
|||||||
Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
||||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
||||||
|
|
||||||
Marker waypoint = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
Marker waypoint = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||||
contentProviderUtils.insertMarker(waypoint);
|
contentProviderUtils.insertMarker(waypoint);
|
||||||
|
|
||||||
ContentResolver contentResolver = context.getContentResolver();
|
ContentResolver contentResolver = context.getContentResolver();
|
||||||
@@ -287,8 +287,7 @@ public class CustomContentProviderUtilsTest {
|
|||||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId2, 10);
|
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId2, 10);
|
||||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId3, 10);
|
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId3, 10);
|
||||||
|
|
||||||
Marker waypoint = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId2));
|
Marker waypoint = new Marker(trackId1, contentProviderUtils.getLastValidTrackPoint(trackId2));
|
||||||
waypoint.setTrackId(trackId1);
|
|
||||||
contentProviderUtils.insertMarker(waypoint);
|
contentProviderUtils.insertMarker(waypoint);
|
||||||
|
|
||||||
ContentResolver contentResolver = context.getContentResolver();
|
ContentResolver contentResolver = context.getContentResolver();
|
||||||
@@ -463,7 +462,7 @@ public class CustomContentProviderUtilsTest {
|
|||||||
track.first.setTrackStatistics(statistics);
|
track.first.setTrackStatistics(statistics);
|
||||||
contentProviderUtils.insertTrack(track.first);
|
contentProviderUtils.insertTrack(track.first);
|
||||||
|
|
||||||
Marker waypoint = new Marker(track.second[0]);
|
Marker waypoint = new Marker(trackId, track.second[0]);
|
||||||
waypoint.setDescription(TEST_DESC);
|
waypoint.setDescription(TEST_DESC);
|
||||||
contentProviderUtils.insertMarker(waypoint);
|
contentProviderUtils.insertMarker(waypoint);
|
||||||
|
|
||||||
@@ -522,9 +521,8 @@ public class CustomContentProviderUtilsTest {
|
|||||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
||||||
|
|
||||||
// Insert at first.
|
// Insert at first.
|
||||||
Marker waypoint1 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
Marker waypoint1 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||||
waypoint1.setDescription(TEST_DESC);
|
waypoint1.setDescription(TEST_DESC);
|
||||||
waypoint1.setTrackId(trackId);
|
|
||||||
contentProviderUtils.insertMarker(waypoint1);
|
contentProviderUtils.insertMarker(waypoint1);
|
||||||
|
|
||||||
// Check insert was done.
|
// Check insert was done.
|
||||||
@@ -600,14 +598,12 @@ public class CustomContentProviderUtilsTest {
|
|||||||
// TestDataUtil.insertTrackWithLocations(contentProviderUtils, track);
|
// TestDataUtil.insertTrackWithLocations(contentProviderUtils, track);
|
||||||
|
|
||||||
// Insert at first.
|
// Insert at first.
|
||||||
Marker waypoint1 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
Marker waypoint1 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||||
waypoint1.setDescription(MOCK_DESC);
|
waypoint1.setDescription(MOCK_DESC);
|
||||||
waypoint1.setTrackId(trackId);
|
|
||||||
Marker.Id waypoint1Id = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint1)));
|
Marker.Id waypoint1Id = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint1)));
|
||||||
|
|
||||||
Marker waypoint2 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
Marker waypoint2 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||||
waypoint2.setDescription(MOCK_DESC);
|
waypoint2.setDescription(MOCK_DESC);
|
||||||
waypoint2.setTrackId(trackId);
|
|
||||||
Marker.Id waypoint2Id = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint2)));
|
Marker.Id waypoint2Id = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint2)));
|
||||||
|
|
||||||
// Delete
|
// Delete
|
||||||
@@ -626,14 +622,10 @@ public class CustomContentProviderUtilsTest {
|
|||||||
Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
||||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
||||||
|
|
||||||
Marker waypoint1 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
Marker waypoint1 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||||
waypoint1.setTrackId(trackId);
|
Marker waypoint2 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||||
Marker waypoint2 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
Marker waypoint3 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||||
waypoint2.setTrackId(trackId);
|
Marker waypoint4 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||||
Marker waypoint3 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
|
||||||
waypoint3.setTrackId(trackId);
|
|
||||||
Marker waypoint4 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
|
||||||
waypoint4.setTrackId(trackId);
|
|
||||||
contentProviderUtils.insertMarker(waypoint1);
|
contentProviderUtils.insertMarker(waypoint1);
|
||||||
contentProviderUtils.insertMarker(waypoint2);
|
contentProviderUtils.insertMarker(waypoint2);
|
||||||
contentProviderUtils.insertMarker(waypoint3);
|
contentProviderUtils.insertMarker(waypoint3);
|
||||||
@@ -651,9 +643,8 @@ public class CustomContentProviderUtilsTest {
|
|||||||
Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
||||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
||||||
|
|
||||||
Marker waypoint = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
Marker waypoint = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||||
waypoint.setDescription(TEST_DESC);
|
waypoint.setDescription(TEST_DESC);
|
||||||
waypoint.setTrackId(trackId);
|
|
||||||
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
|
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
|
||||||
|
|
||||||
assertEquals(TEST_DESC, contentProviderUtils.getMarker(waypointId).getDescription());
|
assertEquals(TEST_DESC, contentProviderUtils.getMarker(waypointId).getDescription());
|
||||||
@@ -668,9 +659,8 @@ public class CustomContentProviderUtilsTest {
|
|||||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
||||||
|
|
||||||
// Insert at first.
|
// Insert at first.
|
||||||
Marker waypoint = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
Marker waypoint = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||||
waypoint.setDescription(TEST_DESC);
|
waypoint.setDescription(TEST_DESC);
|
||||||
waypoint.setTrackId(trackId);
|
|
||||||
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
|
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
@@ -695,7 +685,6 @@ public class CustomContentProviderUtilsTest {
|
|||||||
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
|
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
|
||||||
Marker waypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
|
Marker waypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
|
||||||
waypoint.setDescription(TEST_DESC);
|
waypoint.setDescription(TEST_DESC);
|
||||||
waypoint.setTrackId(trackId);
|
|
||||||
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
|
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
|
||||||
|
|
||||||
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId.getId());
|
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId.getId());
|
||||||
@@ -731,7 +720,6 @@ public class CustomContentProviderUtilsTest {
|
|||||||
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
|
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
|
||||||
Marker waypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
|
Marker waypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
|
||||||
waypoint.setDescription(TEST_DESC);
|
waypoint.setDescription(TEST_DESC);
|
||||||
waypoint.setTrackId(trackId);
|
|
||||||
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
|
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
|
||||||
|
|
||||||
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId.getId());
|
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId.getId());
|
||||||
@@ -766,10 +754,8 @@ public class CustomContentProviderUtilsTest {
|
|||||||
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
|
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
|
||||||
Marker waypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
|
Marker waypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
|
||||||
waypoint.setDescription(TEST_DESC);
|
waypoint.setDescription(TEST_DESC);
|
||||||
waypoint.setTrackId(trackId);
|
|
||||||
Marker otherWaypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
|
Marker otherWaypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
|
||||||
otherWaypoint.setDescription(TEST_DESC);
|
otherWaypoint.setDescription(TEST_DESC);
|
||||||
otherWaypoint.setTrackId(trackId);
|
|
||||||
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
|
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
|
||||||
contentProviderUtils.insertMarker(otherWaypoint);
|
contentProviderUtils.insertMarker(otherWaypoint);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.database.sqlite.SQLiteConstraintException;
|
|||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
|
||||||
@@ -38,39 +37,21 @@ public class CustomSQLiteOpenHelperTest {
|
|||||||
private final Context context = ApplicationProvider.getApplicationContext();
|
private final Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the SQL create statements for all tables (ordered by name).
|
* Get the SQL create statements for all SQLite elements of type (ordered by name).
|
||||||
*
|
*
|
||||||
* @return Map(TableName, SQL)
|
* @param type index, table
|
||||||
|
* @return Map(name, SQL)
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
public static Map<String, String> getSQL(SQLiteDatabase db, String type) {
|
||||||
public static Map<String, String> getTableSQL(SQLiteDatabase db) {
|
HashMap<String, String> sqlMap = new HashMap<>();
|
||||||
HashMap<String, String> tableSQL = new HashMap<>();
|
try (Cursor cursor = db.query("sqlite_master", new String[]{"name", "SQL"}, "type=?", new String[]{type}, null, null, "name")) {
|
||||||
try (Cursor cursor = db.query("sqlite_master", new String[]{"name", "SQL"}, "name IN ('" + TracksColumns.TABLE_NAME + "', '" + TrackPointsColumns.TABLE_NAME + "', '" + MarkerColumns.TABLE_NAME + "')", null, null, null, "name")) {
|
|
||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
tableSQL.put(cursor.getString(0), cursor.getString(1));
|
sqlMap.put(cursor.getString(0), cursor.getString(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tableSQL;
|
return sqlMap;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the SQL create statements for all indices (ordered by name).
|
|
||||||
*
|
|
||||||
* @return Map(tableName, SQL)
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
|
||||||
public static Map<String, String> getIndexSQL(SQLiteDatabase db) {
|
|
||||||
HashMap<String, String> indexSQL = new HashMap<>();
|
|
||||||
try (Cursor cursor = db.rawQuery("SELECT tbl_name, SQL FROM sqlite_master WHERE type = 'index'", null)) {
|
|
||||||
if (cursor != null) {
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
indexSQL.put(cursor.getString(0), cursor.getString(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return indexSQL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,7 +90,7 @@ public class CustomSQLiteOpenHelperTest {
|
|||||||
assertTrue(hasSqlCreate(db, MarkerColumns.CREATE_TABLE));
|
assertTrue(hasSqlCreate(db, MarkerColumns.CREATE_TABLE));
|
||||||
assertTrue(hasSqlCreate(db, MarkerColumns.CREATE_TABLE_INDEX));
|
assertTrue(hasSqlCreate(db, MarkerColumns.CREATE_TABLE_INDEX));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
fail();
|
fail("Database could not be created: " + e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,8 +102,8 @@ public class CustomSQLiteOpenHelperTest {
|
|||||||
Map<String, String> tableByUpgrade;
|
Map<String, String> tableByUpgrade;
|
||||||
Map<String, String> indicesByUpgrade;
|
Map<String, String> indicesByUpgrade;
|
||||||
try (SQLiteDatabase dbUpgraded = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase()) {
|
try (SQLiteDatabase dbUpgraded = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase()) {
|
||||||
tableByUpgrade = getTableSQL(dbUpgraded);
|
tableByUpgrade = getSQL(dbUpgraded, "table");
|
||||||
indicesByUpgrade = getIndexSQL(dbUpgraded);
|
indicesByUpgrade = getSQL(dbUpgraded, "index");
|
||||||
}
|
}
|
||||||
context.deleteDatabase(DATABASE_NAME);
|
context.deleteDatabase(DATABASE_NAME);
|
||||||
|
|
||||||
@@ -130,12 +111,14 @@ public class CustomSQLiteOpenHelperTest {
|
|||||||
Map<String, String> tablesByCreate;
|
Map<String, String> tablesByCreate;
|
||||||
Map<String, String> indicesByCreate;
|
Map<String, String> indicesByCreate;
|
||||||
try (SQLiteDatabase dbCreated = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase()) {
|
try (SQLiteDatabase dbCreated = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase()) {
|
||||||
tablesByCreate = getTableSQL(dbCreated);
|
tablesByCreate = getSQL(dbCreated, "table");
|
||||||
indicesByCreate = getIndexSQL(dbCreated);
|
indicesByCreate = getSQL(dbCreated, "index");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// then - verify table structure
|
// then - verify table structure
|
||||||
assertEquals(3, tableByUpgrade.size());
|
int tableCount = 3 + 2; //Three with data tables + two SQLite
|
||||||
|
assertEquals(tableCount, tableByUpgrade.size());
|
||||||
assertEquals(tableByUpgrade.size(), tablesByCreate.size());
|
assertEquals(tableByUpgrade.size(), tablesByCreate.size());
|
||||||
|
|
||||||
assertEquals(tablesByCreate.get(TracksColumns.TABLE_NAME), tableByUpgrade.get(TracksColumns.TABLE_NAME));
|
assertEquals(tablesByCreate.get(TracksColumns.TABLE_NAME), tableByUpgrade.get(TracksColumns.TABLE_NAME));
|
||||||
@@ -158,8 +141,8 @@ public class CustomSQLiteOpenHelperTest {
|
|||||||
Map<String, String> tablesByDowngrade;
|
Map<String, String> tablesByDowngrade;
|
||||||
Map<String, String> indicesByDowngrade;
|
Map<String, String> indicesByDowngrade;
|
||||||
try (SQLiteDatabase db = new CustomSQLiteOpenHelper(context, DATABASE_NAME, 23).getReadableDatabase()) {
|
try (SQLiteDatabase db = new CustomSQLiteOpenHelper(context, DATABASE_NAME, 23).getReadableDatabase()) {
|
||||||
tablesByDowngrade = getTableSQL(db);
|
tablesByDowngrade = getSQL(db, "table");
|
||||||
indicesByDowngrade = getIndexSQL(db);
|
indicesByDowngrade = getSQL(db, "index");
|
||||||
}
|
}
|
||||||
|
|
||||||
// then - verify table structure
|
// then - verify table structure
|
||||||
|
|||||||
@@ -72,13 +72,12 @@ public class ExportImportTest {
|
|||||||
trackPoints.addAll(Arrays.asList(track.second));
|
trackPoints.addAll(Arrays.asList(track.second));
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
Marker marker = new Marker(track.second[i].getLocation());
|
Marker marker = new Marker(trackId, track.second[i].getLocation());
|
||||||
marker.setName("the marker " + i);
|
marker.setName("the marker " + i);
|
||||||
marker.setDescription("the marker description " + i);
|
marker.setDescription("the marker description " + i);
|
||||||
marker.setCategory("the marker category" + i);
|
marker.setCategory("the marker category" + i);
|
||||||
marker.setIcon("the marker icon" + i);
|
marker.setIcon("the marker icon" + i);
|
||||||
marker.setPhotoUrl("the photo url" + i);
|
marker.setPhotoUrl("the photo url" + i);
|
||||||
marker.setTrackId(trackId);
|
|
||||||
contentProviderUtils.insertMarker(marker);
|
contentProviderUtils.insertMarker(marker);
|
||||||
|
|
||||||
markers.add(marker);
|
markers.add(marker);
|
||||||
@@ -90,7 +89,9 @@ public class ExportImportTest {
|
|||||||
@After
|
@After
|
||||||
public void tearDown() {
|
public void tearDown() {
|
||||||
contentProviderUtils.deleteTrack(context, trackId);
|
contentProviderUtils.deleteTrack(context, trackId);
|
||||||
contentProviderUtils.deleteTrack(context, importTrackId);
|
if (importTrackId != null) {
|
||||||
|
contentProviderUtils.deleteTrack(context, importTrackId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@@ -172,7 +173,7 @@ public class ExportImportTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@Test
|
@Test(expected = ImportAlreadyExistsException.class)
|
||||||
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() {
|
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() {
|
||||||
// given
|
// given
|
||||||
PreferencesUtils.setBoolean(context, R.string.import_prevent_reimport_key, true);
|
PreferencesUtils.setBoolean(context, R.string.import_prevent_reimport_key, true);
|
||||||
@@ -261,7 +262,7 @@ public class ExportImportTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@Test
|
@Test(expected = ImportAlreadyExistsException.class)
|
||||||
public void gpx_duplicate_trackUUID() {
|
public void gpx_duplicate_trackUUID() {
|
||||||
// given
|
// given
|
||||||
PreferencesUtils.setBoolean(context, R.string.import_prevent_reimport_key, true);
|
PreferencesUtils.setBoolean(context, R.string.import_prevent_reimport_key, true);
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ public class IntervalStatisticsTest {
|
|||||||
// given
|
// given
|
||||||
List<TrackPoint> trackPoints = buildTrackPoints(200);
|
List<TrackPoint> trackPoints = buildTrackPoints(200);
|
||||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||||
float distanceInterval = 3000;
|
float distanceInterval = 3000f;
|
||||||
IntervalStatistics intervalStatistics = new IntervalStatistics();
|
IntervalStatistics intervalStatistics = new IntervalStatistics();
|
||||||
|
|
||||||
// when and then
|
// when and then
|
||||||
@@ -97,7 +97,7 @@ public class IntervalStatisticsTest {
|
|||||||
// given
|
// given
|
||||||
List<TrackPoint> trackPoints = buildTrackPoints(1000);
|
List<TrackPoint> trackPoints = buildTrackPoints(1000);
|
||||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||||
float distanceInterval = 3000;
|
float distanceInterval = 3000f;
|
||||||
IntervalStatistics intervalStatistics = new IntervalStatistics();
|
IntervalStatistics intervalStatistics = new IntervalStatistics();
|
||||||
|
|
||||||
// when and then
|
// when and then
|
||||||
@@ -114,7 +114,7 @@ public class IntervalStatisticsTest {
|
|||||||
// given
|
// given
|
||||||
List<TrackPoint> trackPoints = buildTrackPoints(10000);
|
List<TrackPoint> trackPoints = buildTrackPoints(10000);
|
||||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||||
float distanceInterval = 1000;
|
float distanceInterval = 1000f;
|
||||||
IntervalStatistics intervalStatistics = new IntervalStatistics();
|
IntervalStatistics intervalStatistics = new IntervalStatistics();
|
||||||
|
|
||||||
// when and then
|
// when and then
|
||||||
@@ -126,15 +126,18 @@ public class IntervalStatisticsTest {
|
|||||||
List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList();
|
List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList();
|
||||||
double totalDistance = 0d;
|
double totalDistance = 0d;
|
||||||
long totalTime = 0L;
|
long totalTime = 0L;
|
||||||
|
float totalGain = 0f;
|
||||||
for (IntervalStatistics.Interval i : intervalList) {
|
for (IntervalStatistics.Interval i : intervalList) {
|
||||||
totalDistance += i.getDistance_m();
|
totalDistance += i.getDistance_m();
|
||||||
totalTime += ((i.getDistance_m() / i.getSpeed_ms()) * UnitConversions.S_TO_MS);
|
totalTime += ((i.getDistance_m() / i.getSpeed_ms()) * UnitConversions.S_TO_MS);
|
||||||
|
totalGain += i.getGain_m();
|
||||||
}
|
}
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertEquals(trackStatistics.getTotalDistance(), totalDistance, 0.01);
|
assertEquals(trackStatistics.getTotalDistance(), totalDistance, 0.01);
|
||||||
assertEquals(trackStatistics.getTotalTime() * UnitConversions.MS_TO_S, totalTime * UnitConversions.MS_TO_S, 0.1);
|
assertEquals(trackStatistics.getTotalTime() * UnitConversions.MS_TO_S, totalTime * UnitConversions.MS_TO_S, 0.1);
|
||||||
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance() / distanceInterval));
|
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance() / distanceInterval));
|
||||||
|
assertEquals(totalGain, trackPoints.size() * TestDataUtil.ELEVATION_GAIN, 0.1);
|
||||||
for (int i = 0; i < intervalList.size() - 1; i++) {
|
for (int i = 0; i < intervalList.size() - 1; i++) {
|
||||||
assertEquals(intervalList.get(i).getDistance_m(), distanceInterval, 0.001);
|
assertEquals(intervalList.get(i).getDistance_m(), distanceInterval, 0.001);
|
||||||
totalDistance -= intervalList.get(i).getDistance_m();
|
totalDistance -= intervalList.get(i).getDistance_m();
|
||||||
|
|||||||
@@ -68,38 +68,170 @@ limitations under the License.
|
|||||||
|
|
||||||
<activity android:name=".io.file.importer.DirectoryChooserActivity$ImportDirectoryChooserActivity" />
|
<activity android:name=".io.file.importer.DirectoryChooserActivity$ImportDirectoryChooserActivity" />
|
||||||
|
|
||||||
<activity
|
<activity android:name=".io.file.importer.ImportActivity">
|
||||||
android:name=".io.file.importer.ImportActivity"
|
|
||||||
android:theme="@style/ThemeCustomTransparentActivity">
|
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
<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"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:priority="50">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<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.DEFAULT" />
|
||||||
<data android:mimeType="application/vnd.google-earth.kmz" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data
|
<data android:scheme="file"/>
|
||||||
android:scheme="content"
|
<data android:scheme="content"/>
|
||||||
android:host="*"
|
<data android:scheme="data"/>
|
||||||
android:pathPattern="*kmz" />
|
<data android:host="*"/>
|
||||||
|
<data android:mimeType="*/*"/>
|
||||||
|
<data android:pathPattern=".*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter
|
||||||
|
android:label="@string/app_name"
|
||||||
|
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:pathPattern=".*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<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"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:priority="50">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<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.DEFAULT" />
|
||||||
<data android:mimeType="application/vnd.google-earth.kml+xml" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data
|
<data android:scheme="file"/>
|
||||||
android:scheme="content"
|
<data android:scheme="content"/>
|
||||||
android:host="*"
|
<data android:scheme="data"/>
|
||||||
android:pathPattern="*kml" />
|
<data android:host="*"/>
|
||||||
|
<data android:mimeType="*/*"/>
|
||||||
|
<data android:pathPattern=".*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter
|
||||||
|
android:label="@string/app_name"
|
||||||
|
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:pathPattern=".*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<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"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:priority="50">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<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.DEFAULT" />
|
||||||
<data android:mimeType="application/gpx+xml" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data
|
<data android:scheme="file"/>
|
||||||
android:scheme="content"
|
<data android:scheme="content"/>
|
||||||
android:host="*"
|
<data android:scheme="data"/>
|
||||||
android:pathPattern="*gpx" />
|
<data android:host="*"/>
|
||||||
|
<data android:mimeType="*/*"/>
|
||||||
|
<data android:pathPattern=".*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter
|
||||||
|
android:label="@string/app_name"
|
||||||
|
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:pathPattern=".*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
|
||||||
|
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
@@ -152,10 +284,9 @@ limitations under the License.
|
|||||||
<activity android:name=".AggregatedStatisticsActivity" />
|
<activity android:name=".AggregatedStatisticsActivity" />
|
||||||
|
|
||||||
<activity android:name=".io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity" />
|
<activity android:name=".io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity" />
|
||||||
|
<activity android:name=".io.file.importer.DirectoryChooserActivity$DefaultTrackExportDirectoryChooserActivity" />
|
||||||
|
|
||||||
<activity
|
<activity android:name=".io.file.exporter.ExportActivity" />
|
||||||
android:name=".io.file.exporter.ExportActivity"
|
|
||||||
android:theme="@style/ThemeCustomTransparentActivity" />
|
|
||||||
|
|
||||||
<activity android:name=".settings.SettingsActivity" />
|
<activity android:name=".settings.SettingsActivity" />
|
||||||
|
|
||||||
@@ -195,5 +326,15 @@ limitations under the License.
|
|||||||
android:foregroundServiceType="location|connectedDevice"
|
android:foregroundServiceType="location|connectedDevice"
|
||||||
android:icon="@drawable/ic_logo_color_24dp"
|
android:icon="@drawable/ic_logo_color_24dp"
|
||||||
android:label="@string/recording_service" />
|
android:label="@string/recording_service" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".io.file.exporter.ExportService"
|
||||||
|
android:exported="false"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".io.file.importer.ImportService"
|
||||||
|
android:exported="false"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -2,38 +2,35 @@ package de.dennisguse.opentracks;
|
|||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.widget.TextView;
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.databinding.AboutBinding;
|
||||||
import de.dennisguse.opentracks.util.SystemUtils;
|
import de.dennisguse.opentracks.util.SystemUtils;
|
||||||
import de.dennisguse.opentracks.util.ViewUtils;
|
import de.dennisguse.opentracks.util.ViewUtils;
|
||||||
|
|
||||||
public class AboutActivity extends AbstractActivity {
|
public class AboutActivity extends AbstractActivity {
|
||||||
|
|
||||||
|
private AboutBinding viewBinding;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
setTitle(getString(R.string.about_preference_title));
|
setTitle(getString(R.string.about_preference_title));
|
||||||
|
|
||||||
TextView textDescription = findViewById(R.id.about_text_description);
|
viewBinding.aboutTextDescription.setText(getString(R.string.about_description));
|
||||||
textDescription.setText(getString(R.string.about_description));
|
viewBinding.aboutTextVersionName.setText(getString(R.string.about_version_name, SystemUtils.getAppVersionName(this)));
|
||||||
|
viewBinding.aboutTextVersionCode.setText(getString(R.string.about_version_code, SystemUtils.getAppVersionCode(this)));
|
||||||
TextView textVersionName = findViewById(R.id.about_text_version_name);
|
viewBinding.aboutAppUrl.setText(getString(R.string.about_url, getString(R.string.app_web_url)));
|
||||||
textVersionName.setText(getString(R.string.about_version_name, SystemUtils.getAppVersionName(this)));
|
|
||||||
|
|
||||||
TextView textVersionCode = findViewById(R.id.about_text_version_code);
|
|
||||||
textVersionCode.setText(getString(R.string.about_version_code, SystemUtils.getAppVersionCode(this)));
|
|
||||||
|
|
||||||
TextView textURL = findViewById(R.id.about_app_url);
|
|
||||||
textURL.setText(getString(R.string.about_url, getString(R.string.app_web_url)));
|
|
||||||
|
|
||||||
ViewUtils.makeClickableLinks(findViewById(android.R.id.content));
|
ViewUtils.makeClickableLinks(findViewById(android.R.id.content));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int getLayoutResId() {
|
protected View getRootView() {
|
||||||
return R.layout.about;
|
viewBinding = AboutBinding.inflate(getLayoutInflater());
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
package de.dennisguse.opentracks;
|
package de.dennisguse.opentracks;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
@@ -35,14 +36,11 @@ public abstract class AbstractActivity extends AppCompatActivity {
|
|||||||
// Set volume control stream for text to speech
|
// Set volume control stream for text to speech
|
||||||
setVolumeControlStream(TTSUtils.getTTSStream());
|
setVolumeControlStream(TTSUtils.getTTSStream());
|
||||||
|
|
||||||
setContentView(getLayoutResId());
|
setContentView(getRootView());
|
||||||
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected abstract View getRootView();
|
||||||
* Gets the layout resource id.
|
|
||||||
*/
|
|
||||||
protected abstract int getLayoutResId();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,39 @@
|
|||||||
package de.dennisguse.opentracks;
|
package de.dennisguse.opentracks;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.widget.ListView;
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.adapters.AggregatedStatisticsAdapter;
|
import de.dennisguse.opentracks.adapters.AggregatedStatisticsAdapter;
|
||||||
|
import de.dennisguse.opentracks.databinding.AggregatedStatsBinding;
|
||||||
import de.dennisguse.opentracks.viewmodels.AggregatedStatisticsModel;
|
import de.dennisguse.opentracks.viewmodels.AggregatedStatisticsModel;
|
||||||
|
|
||||||
public class AggregatedStatisticsActivity extends AbstractActivity {
|
public class AggregatedStatisticsActivity extends AbstractActivity {
|
||||||
|
|
||||||
private ListView listView;
|
private AggregatedStatsBinding viewBinding;
|
||||||
|
|
||||||
private AggregatedStatisticsAdapter adapter;
|
private AggregatedStatisticsAdapter adapter;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
listView = findViewById(R.id.aggregated_stats_list);
|
viewBinding.aggregatedStatsList.setEmptyView(viewBinding.aggregatedStatsEmptyView);
|
||||||
listView.setEmptyView(findViewById(R.id.aggregated_stats_empty_view));
|
|
||||||
|
|
||||||
final AggregatedStatisticsModel viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class);
|
final AggregatedStatisticsModel viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class);
|
||||||
viewModel.getAggregatedStats().observe(this, aggregatedStatistics -> {
|
viewModel.getAggregatedStats().observe(this, aggregatedStatistics -> {
|
||||||
if (aggregatedStatistics != null) {
|
if (aggregatedStatistics != null) {
|
||||||
adapter = new AggregatedStatisticsAdapter(this, aggregatedStatistics);
|
adapter = new AggregatedStatisticsAdapter(this, aggregatedStatistics);
|
||||||
listView.setAdapter(adapter);
|
viewBinding.aggregatedStatsList.setAdapter(adapter);
|
||||||
}
|
}
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getLayoutResId() {
|
protected View getRootView() {
|
||||||
return R.layout.aggregated_stats;
|
viewBinding = AggregatedStatsBinding.inflate(getLayoutInflater());
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package de.dennisguse.opentracks;
|
package de.dennisguse.opentracks;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.databinding.HelpBinding;
|
||||||
import de.dennisguse.opentracks.util.ViewUtils;
|
import de.dennisguse.opentracks.util.ViewUtils;
|
||||||
|
|
||||||
public class HelpActivity extends AbstractActivity {
|
public class HelpActivity extends AbstractActivity {
|
||||||
@@ -15,7 +17,7 @@ public class HelpActivity extends AbstractActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getLayoutResId() {
|
protected View getRootView() {
|
||||||
return R.layout.help;
|
return HelpBinding.inflate(getLayoutInflater()).getRoot();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ package de.dennisguse.opentracks;
|
|||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -32,6 +33,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import de.dennisguse.opentracks.content.data.Marker;
|
import de.dennisguse.opentracks.content.data.Marker;
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
|
import de.dennisguse.opentracks.databinding.MarkerDetailActivityBinding;
|
||||||
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
||||||
import de.dennisguse.opentracks.fragments.MarkerDetailFragment;
|
import de.dennisguse.opentracks.fragments.MarkerDetailFragment;
|
||||||
|
|
||||||
@@ -48,6 +50,8 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
|
|||||||
|
|
||||||
private static final String TAG = MarkerDetailActivity.class.getSimpleName();
|
private static final String TAG = MarkerDetailActivity.class.getSimpleName();
|
||||||
|
|
||||||
|
private MarkerDetailActivityBinding viewBinding;
|
||||||
|
|
||||||
private List<Marker.Id> markerIds;
|
private List<Marker.Id> markerIds;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -82,10 +86,9 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final ViewPager viewPager = findViewById(R.id.maker_detail_activity_view_pager);
|
|
||||||
final MarkerDetailPagerAdapter markerAdapter = new MarkerDetailPagerAdapter(getSupportFragmentManager());
|
final MarkerDetailPagerAdapter markerAdapter = new MarkerDetailPagerAdapter(getSupportFragmentManager());
|
||||||
viewPager.setAdapter(markerAdapter);
|
viewBinding.makerDetailActivityViewPager.setAdapter(markerAdapter);
|
||||||
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
viewBinding.makerDetailActivityViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||||
@@ -100,12 +103,13 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
|
|||||||
public void onPageScrollStateChanged(int state) {
|
public void onPageScrollStateChanged(int state) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
viewPager.setCurrentItem(markerIndex == -1 ? 0 : markerIndex);
|
viewBinding.makerDetailActivityViewPager.setCurrentItem(markerIndex == -1 ? 0 : markerIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getLayoutResId() {
|
protected View getRootView() {
|
||||||
return R.layout.marker_detail_activity;
|
viewBinding = MarkerDetailActivityBinding.inflate(getLayoutInflater());
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -30,15 +30,10 @@ import android.view.Menu;
|
|||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.AutoCompleteTextView;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.google.android.material.textfield.TextInputEditText;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -46,6 +41,7 @@ import java.io.IOException;
|
|||||||
import de.dennisguse.opentracks.content.data.Marker;
|
import de.dennisguse.opentracks.content.data.Marker;
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
|
import de.dennisguse.opentracks.databinding.MarkerEditBinding;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||||
import de.dennisguse.opentracks.util.FileUtils;
|
import de.dennisguse.opentracks.util.FileUtils;
|
||||||
import de.dennisguse.opentracks.util.IntentUtils;
|
import de.dennisguse.opentracks.util.IntentUtils;
|
||||||
@@ -67,7 +63,7 @@ public class MarkerEditActivity extends AbstractActivity {
|
|||||||
|
|
||||||
private static final String TAG = MarkerEditActivity.class.getSimpleName();
|
private static final String TAG = MarkerEditActivity.class.getSimpleName();
|
||||||
private Track.Id trackId;
|
private Track.Id trackId;
|
||||||
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
|
private final TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
|
||||||
private Marker marker;
|
private Marker marker;
|
||||||
|
|
||||||
private MenuItem insertPhotoMenuItem;
|
private MenuItem insertPhotoMenuItem;
|
||||||
@@ -77,11 +73,7 @@ public class MarkerEditActivity extends AbstractActivity {
|
|||||||
private boolean hasCamera;
|
private boolean hasCamera;
|
||||||
|
|
||||||
// UI elements
|
// UI elements
|
||||||
private TextInputEditText markerName;
|
private MarkerEditBinding viewBinding;
|
||||||
private AutoCompleteTextView markerType;
|
|
||||||
private TextInputEditText markerDescription;
|
|
||||||
private ImageView markerPhoto;
|
|
||||||
private ImageView markerDeletePhotoBtn;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -93,32 +85,24 @@ public class MarkerEditActivity extends AbstractActivity {
|
|||||||
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|
||||||
|
|
||||||
// Setup UI elements
|
// Setup UI elements
|
||||||
markerName = findViewById(R.id.marker_edit_name);
|
|
||||||
markerType = findViewById(R.id.marker_edit_marker_type);
|
|
||||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.marker_types, android.R.layout.simple_dropdown_item_1line);
|
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.marker_types, android.R.layout.simple_dropdown_item_1line);
|
||||||
markerType.setAdapter(adapter);
|
viewBinding.markerEditMarkerType.setAdapter(adapter);
|
||||||
markerDescription = findViewById(R.id.marker_edit_description);
|
viewBinding.markerEditPhotoDelete.setOnClickListener(v -> {
|
||||||
markerPhoto = findViewById(R.id.marker_edit_photo);
|
|
||||||
|
|
||||||
markerDeletePhotoBtn = findViewById(R.id.marker_edit_photo_delete);
|
|
||||||
markerDeletePhotoBtn.setOnClickListener(v -> {
|
|
||||||
if (marker != null && marker.hasPhoto()) {
|
if (marker != null && marker.hasPhoto()) {
|
||||||
marker.setPhotoUrl(null);
|
marker.setPhotoUrl(null);
|
||||||
}
|
}
|
||||||
markerPhoto.setImageBitmap(null);
|
viewBinding.markerEditPhoto.setImageBitmap(null);
|
||||||
photoUri = null;
|
photoUri = null;
|
||||||
hideAndShowOptions();
|
hideAndShowOptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
Button cancel = findViewById(R.id.marker_edit_cancel);
|
viewBinding.markerEditCancel.setOnClickListener(v -> finish());
|
||||||
cancel.setOnClickListener(v -> finish());
|
|
||||||
|
|
||||||
final boolean isNewMarker = markerId == null;
|
final boolean isNewMarker = markerId == null;
|
||||||
|
|
||||||
setTitle(isNewMarker ? R.string.menu_insert_marker : R.string.menu_edit);
|
setTitle(isNewMarker ? R.string.menu_insert_marker : R.string.menu_edit);
|
||||||
Button done = findViewById(R.id.marker_edit_done);
|
viewBinding.markerEditDone.setText(isNewMarker ? R.string.generic_add : R.string.generic_save);
|
||||||
done.setText(isNewMarker ? R.string.generic_add : R.string.generic_save);
|
viewBinding.markerEditDone.setOnClickListener(v -> {
|
||||||
done.setOnClickListener(v -> {
|
|
||||||
if (isNewMarker) {
|
if (isNewMarker) {
|
||||||
addMarker();
|
addMarker();
|
||||||
} else {
|
} else {
|
||||||
@@ -132,10 +116,10 @@ public class MarkerEditActivity extends AbstractActivity {
|
|||||||
if (nextMarkerNumber == -1) {
|
if (nextMarkerNumber == -1) {
|
||||||
nextMarkerNumber = 0;
|
nextMarkerNumber = 0;
|
||||||
}
|
}
|
||||||
markerName.setText(getString(R.string.marker_name_format, nextMarkerNumber));
|
viewBinding.markerEditName.setText(getString(R.string.marker_name_format, nextMarkerNumber));
|
||||||
markerName.selectAll();
|
viewBinding.markerEditName.selectAll();
|
||||||
markerType.setText("");
|
viewBinding.markerEditMarkerType.setText("");
|
||||||
markerDescription.setText("");
|
viewBinding.markerEditDescription.setText("");
|
||||||
} else {
|
} else {
|
||||||
marker = new ContentProviderUtils(this).getMarker(markerId);
|
marker = new ContentProviderUtils(this).getMarker(markerId);
|
||||||
if (marker == null) {
|
if (marker == null) {
|
||||||
@@ -143,9 +127,9 @@ public class MarkerEditActivity extends AbstractActivity {
|
|||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
markerName.setText(marker.getName());
|
viewBinding.markerEditName.setText(marker.getName());
|
||||||
markerType.setText(marker.getCategory());
|
viewBinding.markerEditMarkerType.setText(marker.getCategory());
|
||||||
markerDescription.setText(marker.getDescription());
|
viewBinding.markerEditDescription.setText(marker.getDescription());
|
||||||
if (marker.hasPhoto()) {
|
if (marker.hasPhoto()) {
|
||||||
photoUri = marker.getPhotoURI();
|
photoUri = marker.getPhotoURI();
|
||||||
}
|
}
|
||||||
@@ -183,8 +167,9 @@ public class MarkerEditActivity extends AbstractActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getLayoutResId() {
|
protected View getRootView() {
|
||||||
return R.layout.marker_edit;
|
viewBinding = MarkerEditBinding.inflate(getLayoutInflater());
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -256,7 +241,7 @@ public class MarkerEditActivity extends AbstractActivity {
|
|||||||
insertPhotoMenuItem.setVisible(!isPhotoSet);
|
insertPhotoMenuItem.setVisible(!isPhotoSet);
|
||||||
insertGalleryImgMenuItem.setVisible(!isPhotoSet);
|
insertGalleryImgMenuItem.setVisible(!isPhotoSet);
|
||||||
}
|
}
|
||||||
markerDeletePhotoBtn.setVisibility(isPhotoSet ? View.VISIBLE : View.GONE);
|
viewBinding.markerEditPhotoDelete.setVisibility(isPhotoSet ? View.VISIBLE : View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -270,7 +255,7 @@ public class MarkerEditActivity extends AbstractActivity {
|
|||||||
try (ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r")) {
|
try (ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r")) {
|
||||||
FileDescriptor fd = pfd.getFileDescriptor();
|
FileDescriptor fd = pfd.getFileDescriptor();
|
||||||
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd);
|
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd);
|
||||||
markerPhoto.setImageBitmap(bitmap);
|
viewBinding.markerEditPhoto.setImageBitmap(bitmap);
|
||||||
hideAndShowOptions();
|
hideAndShowOptions();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, e.getMessage());
|
Log.e(TAG, e.getMessage());
|
||||||
@@ -291,16 +276,16 @@ public class MarkerEditActivity extends AbstractActivity {
|
|||||||
|
|
||||||
private void addMarker() {
|
private void addMarker() {
|
||||||
trackRecordingServiceConnection.addMarker(this,
|
trackRecordingServiceConnection.addMarker(this,
|
||||||
markerName.getText().toString(),
|
viewBinding.markerEditName.getText().toString(),
|
||||||
markerType.getText().toString(),
|
viewBinding.markerEditMarkerType.getText().toString(),
|
||||||
markerDescription.getText().toString(),
|
viewBinding.markerEditDescription.getText().toString(),
|
||||||
photoUri != null ? photoUri.toString() : null);
|
photoUri != null ? photoUri.toString() : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveMarker() {
|
private void saveMarker() {
|
||||||
marker.setName(markerName.getText().toString());
|
marker.setName(viewBinding.markerEditName.getText().toString());
|
||||||
marker.setCategory(markerType.getText().toString());
|
marker.setCategory(viewBinding.markerEditMarkerType.getText().toString());
|
||||||
marker.setDescription(markerDescription.getText().toString());
|
marker.setDescription(viewBinding.markerEditDescription.getText().toString());
|
||||||
marker.setPhotoUrl(photoUri != null ? photoUri.toString() : null);
|
marker.setPhotoUrl(photoUri != null ? photoUri.toString() : null);
|
||||||
|
|
||||||
new ContentProviderUtils(this).updateMarker(this, marker);
|
new ContentProviderUtils(this).updateMarker(this, marker);
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import android.view.KeyEvent;
|
|||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ListView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.cursoradapter.widget.ResourceCursorAdapter;
|
import androidx.cursoradapter.widget.ResourceCursorAdapter;
|
||||||
@@ -38,6 +37,7 @@ import de.dennisguse.opentracks.content.data.Marker;
|
|||||||
import de.dennisguse.opentracks.content.data.MarkerColumns;
|
import de.dennisguse.opentracks.content.data.MarkerColumns;
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
|
import de.dennisguse.opentracks.databinding.MarkerListBinding;
|
||||||
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
|
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
|
||||||
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
||||||
import de.dennisguse.opentracks.util.ActivityUtils;
|
import de.dennisguse.opentracks.util.ActivityUtils;
|
||||||
@@ -81,8 +81,9 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
|||||||
};
|
};
|
||||||
private Track track;
|
private Track track;
|
||||||
private ResourceCursorAdapter resourceCursorAdapter;
|
private ResourceCursorAdapter resourceCursorAdapter;
|
||||||
// UI elements
|
|
||||||
private ListView listView;
|
private MarkerListBinding viewBinding;
|
||||||
|
|
||||||
// Callback when an item is selected in the contextual action mode
|
// Callback when an item is selected in the contextual action mode
|
||||||
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
|
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
|
||||||
@Override
|
@Override
|
||||||
@@ -120,9 +121,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
|||||||
|
|
||||||
track = trackId != null ? contentProviderUtils.getTrack(trackId) : null;
|
track = trackId != null ? contentProviderUtils.getTrack(trackId) : null;
|
||||||
|
|
||||||
listView = findViewById(R.id.marker_list);
|
viewBinding.markerList.setEmptyView(viewBinding.markerListEmpty);
|
||||||
listView.setEmptyView(findViewById(R.id.marker_list_empty));
|
viewBinding.markerList.setOnItemClickListener((parent, view, position, id) -> {
|
||||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
|
||||||
Intent intent = IntentUtils.newIntent(MarkerListActivity.this, MarkerDetailActivity.class)
|
Intent intent = IntentUtils.newIntent(MarkerListActivity.this, MarkerDetailActivity.class)
|
||||||
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, new Marker.Id(id));
|
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, new Marker.Id(id));
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
@@ -151,8 +151,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
|||||||
ListItemUtils.setListItem(MarkerListActivity.this, view, false, true, iconId, R.string.image_marker, name, null, null, 0, time, false, category, description, photoUrl);
|
ListItemUtils.setListItem(MarkerListActivity.this, view, false, true, iconId, R.string.image_marker, name, null, null, 0, time, false, category, description, photoUrl);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
listView.setAdapter(resourceCursorAdapter);
|
viewBinding.markerList.setAdapter(resourceCursorAdapter);
|
||||||
ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
|
ActivityUtils.configureListViewContextualMenu(viewBinding.markerList, contextualActionModeCallback);
|
||||||
|
|
||||||
LoaderManager.getInstance(this).initLoader(0, null, new LoaderCallbacks<Cursor>() {
|
LoaderManager.getInstance(this).initLoader(0, null, new LoaderCallbacks<Cursor>() {
|
||||||
@NonNull
|
@NonNull
|
||||||
@@ -193,8 +193,9 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getLayoutResId() {
|
protected View getRootView() {
|
||||||
return R.layout.marker_list;
|
viewBinding = MarkerListBinding.inflate(getLayoutInflater());
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -254,14 +255,14 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
case R.id.list_context_menu_delete:
|
case R.id.list_context_menu_delete:
|
||||||
if (markerIds.length > 1 && markerIds.length == listView.getCount()) {
|
if (markerIds.length > 1 && markerIds.length == viewBinding.markerList.getCount()) {
|
||||||
markerIds = null;
|
markerIds = null;
|
||||||
}
|
}
|
||||||
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), markerIds);
|
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), markerIds);
|
||||||
return true;
|
return true;
|
||||||
case R.id.list_context_menu_select_all:
|
case R.id.list_context_menu_select_all:
|
||||||
for (int i = 0; i < listView.getCount(); i++) {
|
for (int i = 0; i < viewBinding.markerList.getCount(); i++) {
|
||||||
listView.setItemChecked(i, true);
|
viewBinding.markerList.setItemChecked(i, true);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import android.view.Menu;
|
|||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.ListView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
@@ -44,6 +43,7 @@ import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
|
|||||||
import de.dennisguse.opentracks.content.data.Marker;
|
import de.dennisguse.opentracks.content.data.Marker;
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
|
import de.dennisguse.opentracks.databinding.SearchListBinding;
|
||||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||||
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
|
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
|
||||||
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
||||||
@@ -101,6 +101,8 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
|
|||||||
|
|
||||||
private boolean recordingTrackPaused;
|
private boolean recordingTrackPaused;
|
||||||
|
|
||||||
|
private SearchListBinding viewBinding;
|
||||||
|
|
||||||
// Callback when an item is selected in the contextual action mode
|
// Callback when an item is selected in the contextual action mode
|
||||||
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
|
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
|
||||||
@Override
|
@Override
|
||||||
@@ -198,10 +200,9 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// UI elements
|
// UI elements
|
||||||
ListView listView = findViewById(R.id.search_list);
|
viewBinding.searchList.setEmptyView(viewBinding.searchListEmpty);
|
||||||
listView.setAdapter(arrayAdapter);
|
viewBinding.searchList.setAdapter(arrayAdapter);
|
||||||
listView.setEmptyView(findViewById(R.id.search_list_empty));
|
viewBinding.searchList.setOnItemClickListener((parent, view, position, id) -> {
|
||||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
|
||||||
Map<String, Object> item = arrayAdapter.getItem(position);
|
Map<String, Object> item = arrayAdapter.getItem(position);
|
||||||
Track.Id trackId = (Track.Id) item.get(TRACK_ID_FIELD);
|
Track.Id trackId = (Track.Id) item.get(TRACK_ID_FIELD);
|
||||||
Marker.Id markerId = (Marker.Id) item.get(MARKER_ID_FIELD);
|
Marker.Id markerId = (Marker.Id) item.get(MARKER_ID_FIELD);
|
||||||
@@ -213,7 +214,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
|
|||||||
}
|
}
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
});
|
});
|
||||||
ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
|
ActivityUtils.configureListViewContextualMenu(viewBinding.searchList, contextualActionModeCallback);
|
||||||
handleIntent(getIntent());
|
handleIntent(getIntent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,8 +240,9 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getLayoutResId() {
|
protected View getRootView() {
|
||||||
return R.layout.search_list;
|
viewBinding = SearchListBinding.inflate(getLayoutInflater());
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ import android.view.MotionEvent;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.databinding.TrackControllerBinding;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||||
import de.dennisguse.opentracks.util.ActivityUtils;
|
import de.dennisguse.opentracks.util.ActivityUtils;
|
||||||
@@ -49,14 +49,11 @@ public class TrackController {
|
|||||||
private final Activity activity;
|
private final Activity activity;
|
||||||
private final TrackRecordingServiceConnection trackRecordingServiceConnection;
|
private final TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||||
private final Handler handlerUpdateTotalTime;
|
private final Handler handlerUpdateTotalTime;
|
||||||
private final View containerView;
|
|
||||||
private final TextView statusTextView;
|
|
||||||
private final TextView totalTimeTextView;
|
|
||||||
private final ImageButton recordImageButton;
|
|
||||||
private final ImageButton stopImageButton;
|
|
||||||
private final boolean alwaysShow;
|
private final boolean alwaysShow;
|
||||||
private ButtonDelay buttonDelay;
|
private ButtonDelay buttonDelay;
|
||||||
|
|
||||||
|
private final TrackControllerBinding viewBinding;
|
||||||
|
|
||||||
private boolean isRecording;
|
private boolean isRecording;
|
||||||
private boolean isPaused;
|
private boolean isPaused;
|
||||||
private long totalTime = 0;
|
private long totalTime = 0;
|
||||||
@@ -69,37 +66,32 @@ public class TrackController {
|
|||||||
private final Runnable updateTotalTimeRunnable = new Runnable() {
|
private final Runnable updateTotalTimeRunnable = new Runnable() {
|
||||||
public void run() {
|
public void run() {
|
||||||
if (isResumed && isRecording && !isPaused) {
|
if (isResumed && isRecording && !isPaused) {
|
||||||
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(System.currentTimeMillis() - totalTimeTimestamp + totalTime));
|
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(System.currentTimeMillis() - totalTimeTimestamp + totalTime));
|
||||||
handlerUpdateTotalTime.postDelayed(this, UnitConversions.ONE_SECOND_MS);
|
handlerUpdateTotalTime.postDelayed(this, UnitConversions.ONE_SECOND_MS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
TrackController(Activity activity, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, OnClickListener recordListener, OnClickListener stopListener) {
|
TrackController(Activity activity, TrackControllerBinding viewBinding, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, OnClickListener recordListener, OnClickListener stopListener) {
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
|
this.viewBinding = viewBinding;
|
||||||
this.trackRecordingServiceConnection = trackRecordingServiceConnection;
|
this.trackRecordingServiceConnection = trackRecordingServiceConnection;
|
||||||
this.alwaysShow = alwaysShow;
|
this.alwaysShow = alwaysShow;
|
||||||
handlerUpdateTotalTime = new Handler();
|
handlerUpdateTotalTime = new Handler();
|
||||||
containerView = activity.findViewById(R.id.track_controller_container);
|
viewBinding.trackControllerRecord.setOnTouchListener((view, motionEvent) -> onRecordTouch(activity, recordListener, motionEvent));
|
||||||
statusTextView = activity.findViewById(R.id.track_controller_status);
|
|
||||||
totalTimeTextView = activity.findViewById(R.id.track_controller_total_time);
|
|
||||||
|
|
||||||
recordImageButton = activity.findViewById(R.id.track_controller_record);
|
viewBinding.trackControllerStop.setOnTouchListener((view, motionEvent) -> onStopTouch(activity, stopListener, motionEvent));
|
||||||
recordImageButton.setOnTouchListener((view, motionEvent) -> onRecordTouch(activity, recordListener, motionEvent));
|
|
||||||
|
|
||||||
stopImageButton = activity.findViewById(R.id.track_controller_stop);
|
|
||||||
stopImageButton.setOnTouchListener((view, motionEvent) -> onStopTouch(activity, stopListener, motionEvent));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean onRecordTouch(final Activity activity, final OnClickListener recordListener, final MotionEvent motionEvent) {
|
private boolean onRecordTouch(final Activity activity, final OnClickListener recordListener, final MotionEvent motionEvent) {
|
||||||
if (isRecording && !isPaused) {
|
if (isRecording && !isPaused) {
|
||||||
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
|
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
|
||||||
buttonDelay = new ButtonDelay(activity, recordImageButton, R.drawable.ic_button_pause_anim, R.string.hold_to_pause, recordListener);
|
buttonDelay = new ButtonDelay(activity, viewBinding.trackControllerRecord, R.drawable.ic_button_pause_anim, R.string.hold_to_pause, recordListener);
|
||||||
new Thread(buttonDelay).start();
|
new Thread(buttonDelay).start();
|
||||||
return true;
|
return true;
|
||||||
} else if (motionEvent.getAction() == MotionEvent.ACTION_UP ) {
|
} else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
|
||||||
recordImageButton.setImageResource(R.drawable.ic_button_pause);
|
viewBinding.trackControllerRecord.setImageResource(R.drawable.ic_button_pause);
|
||||||
if (buttonDelay != null) {
|
if (buttonDelay != null) {
|
||||||
buttonDelay.canceled = true;
|
buttonDelay.canceled = true;
|
||||||
}
|
}
|
||||||
@@ -115,11 +107,11 @@ public class TrackController {
|
|||||||
private boolean onStopTouch(final Activity activity, final OnClickListener stopListener, final MotionEvent motionEvent) {
|
private boolean onStopTouch(final Activity activity, final OnClickListener stopListener, final MotionEvent motionEvent) {
|
||||||
if (isRecording) {
|
if (isRecording) {
|
||||||
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
|
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
|
||||||
buttonDelay = new ButtonDelay(activity, stopImageButton, R.drawable.ic_button_stop_anim, R.string.hold_to_stop, stopListener);
|
buttonDelay = new ButtonDelay(activity, viewBinding.trackControllerStop, R.drawable.ic_button_stop_anim, R.string.hold_to_stop, stopListener);
|
||||||
new Thread(buttonDelay).start();
|
new Thread(buttonDelay).start();
|
||||||
return true;
|
return true;
|
||||||
} else if (motionEvent.getAction() == MotionEvent.ACTION_UP ) {
|
} else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
|
||||||
stopImageButton.setImageResource(R.drawable.ic_button_stop);
|
viewBinding.trackControllerStop.setImageResource(R.drawable.ic_button_stop);
|
||||||
if (buttonDelay != null) {
|
if (buttonDelay != null) {
|
||||||
buttonDelay.canceled = true;
|
buttonDelay.canceled = true;
|
||||||
}
|
}
|
||||||
@@ -151,7 +143,7 @@ public class TrackController {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
activity.runOnUiThread(()->{
|
activity.runOnUiThread(() -> {
|
||||||
imageButton.setImageDrawable(drawable);
|
imageButton.setImageDrawable(drawable);
|
||||||
if (drawable instanceof AnimatedVectorDrawable) {
|
if (drawable instanceof AnimatedVectorDrawable) {
|
||||||
((AnimatedVectorDrawable) drawable).start();
|
((AnimatedVectorDrawable) drawable).start();
|
||||||
@@ -165,7 +157,7 @@ public class TrackController {
|
|||||||
} catch (InterruptedException ignored) {
|
} catch (InterruptedException ignored) {
|
||||||
}
|
}
|
||||||
if (!canceled) {
|
if (!canceled) {
|
||||||
activity.runOnUiThread(()-> {
|
activity.runOnUiThread(() -> {
|
||||||
clickListener.onClick(null);
|
clickListener.onClick(null);
|
||||||
ActivityUtils.vibrate(activity, 1000);
|
ActivityUtils.vibrate(activity, 1000);
|
||||||
});
|
});
|
||||||
@@ -180,30 +172,30 @@ public class TrackController {
|
|||||||
isRecording = recording;
|
isRecording = recording;
|
||||||
isPaused = paused;
|
isPaused = paused;
|
||||||
boolean visible = alwaysShow || isRecording;
|
boolean visible = alwaysShow || isRecording;
|
||||||
containerView.setVisibility(visible ? View.VISIBLE : View.GONE);
|
viewBinding.trackControllerContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
|
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
recordImageButton.setImageResource(isRecording && !isPaused ? R.drawable.ic_button_pause : R.drawable.button_record);
|
viewBinding.trackControllerRecord.setImageResource(isRecording && !isPaused ? R.drawable.ic_button_pause : R.drawable.button_record);
|
||||||
recordImageButton.setContentDescription(activity.getString(isRecording && !isPaused ? R.string.image_pause : R.string.image_record));
|
viewBinding.trackControllerRecord.setContentDescription(activity.getString(isRecording && !isPaused ? R.string.image_pause : R.string.image_record));
|
||||||
|
|
||||||
stopImageButton.setImageResource(isRecording ? R.drawable.ic_button_stop : R.drawable.ic_button_stop_disabled);
|
viewBinding.trackControllerStop.setImageResource(isRecording ? R.drawable.ic_button_stop : R.drawable.ic_button_stop_disabled);
|
||||||
stopImageButton.setEnabled(isRecording);
|
viewBinding.trackControllerStop.setEnabled(isRecording);
|
||||||
|
|
||||||
statusTextView.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
|
viewBinding.trackControllerStatus.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
|
||||||
if (isRecording) {
|
if (isRecording) {
|
||||||
statusTextView.setTextColor(activity.getResources().getColor(isPaused ? android.R.color.white : R.color.recording_text));
|
viewBinding.trackControllerStatus.setTextColor(activity.getResources().getColor(isPaused ? android.R.color.white : R.color.recording_text));
|
||||||
statusTextView.setText(isPaused ? R.string.generic_paused : R.string.generic_recording);
|
viewBinding.trackControllerStatus.setText(isPaused ? R.string.generic_paused : R.string.generic_recording);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
|
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
|
||||||
totalTimeTextView.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
|
viewBinding.trackControllerTotalTime.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
|
||||||
if (isRecording) {
|
if (isRecording) {
|
||||||
totalTime = getTotalTime();
|
totalTime = getTotalTime();
|
||||||
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
|
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
|
||||||
if (!isPaused) {
|
if (!isPaused) {
|
||||||
totalTimeTimestamp = System.currentTimeMillis();
|
totalTimeTimestamp = System.currentTimeMillis();
|
||||||
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND_MS);
|
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND_MS);
|
||||||
@@ -222,11 +214,11 @@ public class TrackController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void hide() {
|
public void hide() {
|
||||||
containerView.setVisibility(View.GONE);
|
viewBinding.trackControllerContainer.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void show() {
|
public void show() {
|
||||||
containerView.setVisibility(View.VISIBLE);
|
viewBinding.trackControllerContainer.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
|
import de.dennisguse.opentracks.databinding.TrackDeleteBinding;
|
||||||
import de.dennisguse.opentracks.util.SystemUtils;
|
import de.dennisguse.opentracks.util.SystemUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +41,8 @@ public class TrackDeleteActivity extends AbstractActivity {
|
|||||||
|
|
||||||
private Thread deleteThread;
|
private Thread deleteThread;
|
||||||
|
|
||||||
|
private TrackDeleteBinding viewBinding;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -52,13 +55,7 @@ public class TrackDeleteActivity extends AbstractActivity {
|
|||||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(TrackDeleteActivity.this);
|
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(TrackDeleteActivity.this);
|
||||||
|
|
||||||
PowerManager.WakeLock wakeLock = SystemUtils.acquireWakeLock(TrackDeleteActivity.this, null);
|
PowerManager.WakeLock wakeLock = SystemUtils.acquireWakeLock(TrackDeleteActivity.this, null);
|
||||||
|
contentProviderUtils.deleteTracks(TrackDeleteActivity.this, trackIds);
|
||||||
for (Track.Id id : trackIds) {
|
|
||||||
if (Thread.interrupted()) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
contentProviderUtils.deleteTrack(TrackDeleteActivity.this, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
|
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
|
||||||
if (Thread.interrupted()) {
|
if (Thread.interrupted()) {
|
||||||
@@ -82,12 +79,13 @@ public class TrackDeleteActivity extends AbstractActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getLayoutResId() {
|
protected View getRootView() {
|
||||||
return R.layout.track_delete;
|
viewBinding = TrackDeleteBinding.inflate(getLayoutInflater());
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onAsyncTaskCompleted() {
|
public void onAsyncTaskCompleted() {
|
||||||
findViewById(R.id.progressbar).setVisibility(View.INVISIBLE);
|
viewBinding.progressbar.setVisibility(View.INVISIBLE);
|
||||||
setResult(RESULT_OK);
|
setResult(RESULT_OK);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,16 +22,12 @@ import android.view.KeyEvent;
|
|||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.AutoCompleteTextView;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.google.android.material.textfield.TextInputEditText;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
|
import de.dennisguse.opentracks.databinding.TrackEditBinding;
|
||||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||||
@@ -56,10 +52,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
|||||||
private Track track;
|
private Track track;
|
||||||
private String iconValue;
|
private String iconValue;
|
||||||
|
|
||||||
private TextInputEditText nameEditText;
|
private TrackEditBinding viewBinding;
|
||||||
private AutoCompleteTextView activityType;
|
|
||||||
private Spinner activityTypeIcon;
|
|
||||||
private TextInputEditText description;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle bundle) {
|
protected void onCreate(Bundle bundle) {
|
||||||
@@ -81,20 +74,17 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
nameEditText = findViewById(R.id.track_edit_name);
|
viewBinding.trackEditName.setText(track.getName());
|
||||||
nameEditText.setText(track.getName());
|
|
||||||
|
|
||||||
activityType = findViewById(R.id.track_edit_activity_type);
|
viewBinding.trackEditActivityType.setText(track.getCategory());
|
||||||
activityType.setText(track.getCategory());
|
|
||||||
|
|
||||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
|
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
|
||||||
activityType.setAdapter(adapter);
|
viewBinding.trackEditActivityType.setAdapter(adapter);
|
||||||
activityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(
|
viewBinding.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.trackEditActivityType.getAdapter().getItem(position))));
|
||||||
TrackEditActivity.this, (String) activityType.getAdapter().getItem(position))));
|
viewBinding.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
activityType.setOnFocusChangeListener((v, hasFocus) -> {
|
|
||||||
if (!hasFocus) {
|
if (!hasFocus) {
|
||||||
setActivityTypeIcon(TrackIconUtils.getIconValue(
|
setActivityTypeIcon(TrackIconUtils.getIconValue(
|
||||||
TrackEditActivity.this, activityType.getText().toString()));
|
TrackEditActivity.this, viewBinding.trackEditActivityType.getText().toString()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,40 +96,36 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
|||||||
iconValue = track.getIcon();
|
iconValue = track.getIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
activityTypeIcon = findViewById(R.id.track_edit_activity_type_icon);
|
viewBinding.trackEditActivityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(this, iconValue));
|
||||||
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(this, iconValue));
|
viewBinding.trackEditActivityTypeIcon.setOnTouchListener((v, event) -> {
|
||||||
activityTypeIcon.setOnTouchListener((v, event) -> {
|
|
||||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||||
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
|
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.trackEditActivityType.getText().toString());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
activityTypeIcon.setOnKeyListener((v, keyCode, event) -> {
|
viewBinding.trackEditActivityTypeIcon.setOnKeyListener((v, keyCode, event) -> {
|
||||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
|
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
|
||||||
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
|
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.trackEditActivityType.getText().toString());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
description = findViewById(R.id.track_edit_description);
|
viewBinding.trackEditDescription.setText(track.getDescription());
|
||||||
description.setText(track.getDescription());
|
|
||||||
|
|
||||||
Button saveButton = findViewById(R.id.track_edit_save);
|
viewBinding.trackEditSave.setOnClickListener(v -> {
|
||||||
saveButton.setOnClickListener(v -> {
|
TrackUtils.updateTrack(TrackEditActivity.this, track, viewBinding.trackEditName.getText().toString(),
|
||||||
TrackUtils.updateTrack(TrackEditActivity.this, track, nameEditText.getText().toString(),
|
viewBinding.trackEditActivityType.getText().toString(), viewBinding.trackEditDescription.getText().toString(),
|
||||||
activityType.getText().toString(), description.getText().toString(),
|
|
||||||
contentProviderUtils);
|
contentProviderUtils);
|
||||||
finish();
|
finish();
|
||||||
});
|
});
|
||||||
|
|
||||||
Button cancel = findViewById(R.id.track_edit_cancel);
|
|
||||||
if (getIntent().getBooleanExtra(EXTRA_NEW_TRACK, false)) {
|
if (getIntent().getBooleanExtra(EXTRA_NEW_TRACK, false)) {
|
||||||
setTitle(R.string.track_edit_new_track_title);
|
setTitle(R.string.track_edit_new_track_title);
|
||||||
cancel.setVisibility(View.GONE);
|
viewBinding.trackEditCancel.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
setTitle(R.string.menu_edit);
|
setTitle(R.string.menu_edit);
|
||||||
cancel.setOnClickListener(v -> finish());
|
viewBinding.trackEditCancel.setOnClickListener(v -> finish());
|
||||||
cancel.setVisibility(View.VISIBLE);
|
viewBinding.trackEditCancel.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,18 +149,19 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getLayoutResId() {
|
protected View getRootView() {
|
||||||
return R.layout.track_edit;
|
viewBinding = TrackEditBinding.inflate(getLayoutInflater());
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setActivityTypeIcon(String value) {
|
private void setActivityTypeIcon(String iconValue) {
|
||||||
iconValue = value;
|
this.iconValue = iconValue;
|
||||||
TrackIconUtils.setIconSpinner(activityTypeIcon, value);
|
TrackIconUtils.setIconSpinner(viewBinding.trackEditActivityTypeIcon, iconValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onChooseActivityTypeDone(String value) {
|
public void onChooseActivityTypeDone(String value) {
|
||||||
setActivityTypeIcon(value);
|
setActivityTypeIcon(value);
|
||||||
activityType.setText(getString(TrackIconUtils.getIconActivityType(value)));
|
viewBinding.trackEditActivityType.setText(getString(TrackIconUtils.getIconActivityType(value)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import android.view.Menu;
|
|||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -43,11 +42,12 @@ import androidx.core.app.ActivityCompat;
|
|||||||
import androidx.cursoradapter.widget.ResourceCursorAdapter;
|
import androidx.cursoradapter.widget.ResourceCursorAdapter;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.loader.app.LoaderManager;
|
||||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||||
|
import androidx.loader.content.CursorLoader;
|
||||||
import androidx.loader.content.Loader;
|
import androidx.loader.content.Loader;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
import de.dennisguse.opentracks.databinding.TrackListBinding;
|
||||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||||
@@ -60,6 +60,7 @@ import de.dennisguse.opentracks.util.ListItemUtils;
|
|||||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||||
import de.dennisguse.opentracks.util.StringUtils;
|
import de.dennisguse.opentracks.util.StringUtils;
|
||||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||||
|
import de.dennisguse.opentracks.util.TrackUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An activity displaying a list of tracks.
|
* An activity displaying a list of tracks.
|
||||||
@@ -71,18 +72,22 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
private static final String TAG = TrackListActivity.class.getSimpleName();
|
private static final String TAG = TrackListActivity.class.getSimpleName();
|
||||||
|
|
||||||
// The following are set in onCreate
|
// The following are set in onCreate
|
||||||
private ContentProviderUtils contentProviderUtils;
|
|
||||||
private SharedPreferences sharedPreferences;
|
private SharedPreferences sharedPreferences;
|
||||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||||
private TrackController trackController;
|
private TrackController trackController;
|
||||||
private ListView listView;
|
|
||||||
private ResourceCursorAdapter resourceCursorAdapter;
|
private ResourceCursorAdapter resourceCursorAdapter;
|
||||||
private GpsStatusValue gpsStatusValue;
|
private GpsStatusValue gpsStatusValue;
|
||||||
|
|
||||||
|
private TrackListBinding viewBinding;
|
||||||
|
|
||||||
private final LoaderCallbacks<Cursor> loaderCallbacks = new LoaderCallbacks<Cursor>() {
|
private final LoaderCallbacks<Cursor> loaderCallbacks = new LoaderCallbacks<Cursor>() {
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
|
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
|
||||||
return ContentProviderUtils.getTracksCursorLoader(TrackListActivity.this);
|
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
|
@Override
|
||||||
@@ -223,11 +228,10 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
|
|
||||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
|
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
|
||||||
|
|
||||||
contentProviderUtils = new ContentProviderUtils(this);
|
|
||||||
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
||||||
|
|
||||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||||
trackController = new TrackController(this, trackRecordingServiceConnection, true, recordListener, stopListener);
|
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, true, recordListener, stopListener);
|
||||||
|
|
||||||
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
||||||
|
|
||||||
@@ -237,9 +241,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
searchManager.setOnDismissListener(() -> trackController.show());
|
searchManager.setOnDismissListener(() -> trackController.show());
|
||||||
}
|
}
|
||||||
|
|
||||||
listView = findViewById(R.id.track_list);
|
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
|
||||||
listView.setEmptyView(findViewById(R.id.track_list_empty_view));
|
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> {
|
||||||
listView.setOnItemClickListener((parent, view, position, trackId) -> {
|
|
||||||
Intent newIntent;
|
Intent newIntent;
|
||||||
if (trackId == recordingTrackId.getId()) {
|
if (trackId == recordingTrackId.getId()) {
|
||||||
// Is recording -> open record activity.
|
// Is recording -> open record activity.
|
||||||
@@ -256,14 +259,15 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
|
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
|
||||||
@Override
|
@Override
|
||||||
public void bindView(View view, Context context, Cursor cursor) {
|
public void bindView(View view, Context context, Cursor cursor) {
|
||||||
int idIndex = cursor.getColumnIndex(TracksColumns._ID);
|
int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID);
|
||||||
int iconIndex = cursor.getColumnIndex(TracksColumns.ICON);
|
int iconIndex = cursor.getColumnIndexOrThrow(TracksColumns.ICON);
|
||||||
int nameIndex = cursor.getColumnIndex(TracksColumns.NAME);
|
int nameIndex = cursor.getColumnIndexOrThrow(TracksColumns.NAME);
|
||||||
int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
|
int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
|
||||||
int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
|
int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
|
||||||
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
|
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
|
||||||
int categoryIndex = cursor.getColumnIndex(TracksColumns.CATEGORY);
|
int categoryIndex = cursor.getColumnIndexOrThrow(TracksColumns.CATEGORY);
|
||||||
int descriptionIndex = cursor.getColumnIndex(TracksColumns.DESCRIPTION);
|
int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION);
|
||||||
|
int markerCountIndex = cursor.getColumnIndexOrThrow(TracksColumns.MARKER_COUNT);
|
||||||
|
|
||||||
Track.Id trackId = new Track.Id(cursor.getLong(idIndex));
|
Track.Id trackId = new Track.Id(cursor.getLong(idIndex));
|
||||||
boolean isRecording = trackId.equals(recordingTrackId);
|
boolean isRecording = trackId.equals(recordingTrackId);
|
||||||
@@ -272,7 +276,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
String name = cursor.getString(nameIndex);
|
String name = cursor.getString(nameIndex);
|
||||||
String totalTime = StringUtils.formatElapsedTime(cursor.getLong(totalTimeIndex));
|
String totalTime = StringUtils.formatElapsedTime(cursor.getLong(totalTimeIndex));
|
||||||
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, cursor.getDouble(totalDistanceIndex), metricUnits);
|
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, cursor.getDouble(totalDistanceIndex), metricUnits);
|
||||||
int markerCount = contentProviderUtils.getMarkerCount(trackId);
|
int markerCount = cursor.getInt(markerCountIndex);
|
||||||
long startTime = cursor.getLong(startTimeIndex);
|
long startTime = cursor.getLong(startTimeIndex);
|
||||||
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
|
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
|
||||||
String description = cursor.getString(descriptionIndex);
|
String description = cursor.getString(descriptionIndex);
|
||||||
@@ -282,9 +286,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
startTime, true, category, description, null);
|
startTime, true, category, description, null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
listView.setAdapter(resourceCursorAdapter);
|
viewBinding.trackList.setAdapter(resourceCursorAdapter);
|
||||||
|
|
||||||
ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
|
ActivityUtils.configureListViewContextualMenu(viewBinding.trackList, contextualActionModeCallback);
|
||||||
|
|
||||||
LoaderManager.getInstance(this).initLoader(0, null, loaderCallbacks);
|
LoaderManager.getInstance(this).initLoader(0, null, loaderCallbacks);
|
||||||
|
|
||||||
@@ -338,8 +342,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getLayoutResId() {
|
protected View getRootView() {
|
||||||
return R.layout.track_list;
|
viewBinding = TrackListBinding.inflate(getLayoutInflater());
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -503,9 +508,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
|||||||
deleteTracks(trackIds);
|
deleteTracks(trackIds);
|
||||||
return true;
|
return true;
|
||||||
case R.id.list_context_menu_select_all:
|
case R.id.list_context_menu_select_all:
|
||||||
int size = listView.getCount();
|
int size = viewBinding.trackList.getCount();
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
listView.setItemChecked(i, true);
|
viewBinding.trackList.setItemChecked(i, true);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,21 +21,19 @@ import android.os.Bundle;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentPagerAdapter;
|
import androidx.fragment.app.FragmentPagerAdapter;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
|
||||||
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.content.TrackDataHub;
|
import de.dennisguse.opentracks.content.TrackDataHub;
|
||||||
import de.dennisguse.opentracks.content.data.Marker;
|
import de.dennisguse.opentracks.content.data.Marker;
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
|
import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
|
||||||
import de.dennisguse.opentracks.fragments.ChartFragment;
|
import de.dennisguse.opentracks.fragments.ChartFragment;
|
||||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
|
||||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||||
import de.dennisguse.opentracks.fragments.IntervalsFragment;
|
import de.dennisguse.opentracks.fragments.IntervalsFragment;
|
||||||
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
|
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
|
||||||
@@ -44,8 +42,6 @@ import de.dennisguse.opentracks.settings.SettingsActivity;
|
|||||||
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
import de.dennisguse.opentracks.util.IntentDashboardUtils;
|
||||||
import de.dennisguse.opentracks.util.IntentUtils;
|
import de.dennisguse.opentracks.util.IntentUtils;
|
||||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
|
||||||
import de.dennisguse.opentracks.util.TrackUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An activity to show the track detail, record a new track or resumes an existing one.
|
* An activity to show the track detail, record a new track or resumes an existing one.
|
||||||
@@ -53,7 +49,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
|
|||||||
* @author Leif Hendrik Wilden
|
* @author Leif Hendrik Wilden
|
||||||
* @author Rodrigo Damazio
|
* @author Rodrigo Damazio
|
||||||
*/
|
*/
|
||||||
public class TrackRecordedActivity extends AbstractListActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
|
public class TrackRecordedActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
|
||||||
|
|
||||||
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
|
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
|
||||||
|
|
||||||
@@ -65,7 +61,8 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
|
|||||||
// The following are set in onCreate.
|
// The following are set in onCreate.
|
||||||
private ContentProviderUtils contentProviderUtils;
|
private ContentProviderUtils contentProviderUtils;
|
||||||
private TrackDataHub trackDataHub;
|
private TrackDataHub trackDataHub;
|
||||||
private ViewPager pager;
|
|
||||||
|
private TrackRecordedBinding viewBinding;
|
||||||
|
|
||||||
private Track.Id trackId;
|
private Track.Id trackId;
|
||||||
|
|
||||||
@@ -78,12 +75,10 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
|
|||||||
|
|
||||||
trackDataHub = new TrackDataHub(this);
|
trackDataHub = new TrackDataHub(this);
|
||||||
|
|
||||||
pager = findViewById(R.id.track_detail_activity_view_pager);
|
viewBinding.trackDetailActivityViewPager.setAdapter(new CustomFragmentPagerAdapter(getSupportFragmentManager(), FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT));
|
||||||
pager.setAdapter(new CustomFragmentPagerAdapter(getSupportFragmentManager(), FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT));
|
viewBinding.trackDetailActivityTablayout.setupWithViewPager(viewBinding.trackDetailActivityViewPager);
|
||||||
TabLayout tabs = findViewById(R.id.track_detail_activity_tablayout);
|
|
||||||
tabs.setupWithViewPager(pager);
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
pager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
|
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,12 +109,13 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
|
|||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putInt(CURRENT_TAB_TAG_KEY, pager.getCurrentItem());
|
outState.putInt(CURRENT_TAB_TAG_KEY, viewBinding.trackDetailActivityViewPager.getCurrentItem());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getLayoutResId() {
|
protected View getRootView() {
|
||||||
return R.layout.track_detail;
|
viewBinding = TrackRecordedBinding.inflate(getLayoutInflater());
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -224,17 +220,6 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void chooseActivityType(String category) {
|
|
||||||
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), category);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onChooseActivityTypeDone(String iconValue) {
|
|
||||||
String category = getString(TrackIconUtils.getIconActivityType(iconValue));
|
|
||||||
Track track = contentProviderUtils.getTrack(trackId);
|
|
||||||
TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CustomFragmentPagerAdapter extends FragmentPagerAdapter {
|
private class CustomFragmentPagerAdapter extends FragmentPagerAdapter {
|
||||||
|
|
||||||
public CustomFragmentPagerAdapter(@NonNull FragmentManager fm, int behavior) {
|
public CustomFragmentPagerAdapter(@NonNull FragmentManager fm, int behavior) {
|
||||||
@@ -253,7 +238,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
|
|||||||
case 0:
|
case 0:
|
||||||
return StatisticsRecordedFragment.newInstance(trackId);
|
return StatisticsRecordedFragment.newInstance(trackId);
|
||||||
case 1:
|
case 1:
|
||||||
return IntervalsFragment.newInstance(trackId);
|
return IntervalsFragment.newInstance();
|
||||||
case 2:
|
case 2:
|
||||||
return ChartFragment.newInstance(false);
|
return ChartFragment.newInstance(false);
|
||||||
case 3:
|
case 3:
|
||||||
|
|||||||
@@ -16,13 +16,11 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentPagerAdapter;
|
import androidx.fragment.app.FragmentPagerAdapter;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
|
||||||
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.content.TrackDataHub;
|
import de.dennisguse.opentracks.content.TrackDataHub;
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
|
import de.dennisguse.opentracks.databinding.TrackRecordingBinding;
|
||||||
import de.dennisguse.opentracks.fragments.ChartFragment;
|
import de.dennisguse.opentracks.fragments.ChartFragment;
|
||||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||||
import de.dennisguse.opentracks.fragments.IntervalsFragment;
|
import de.dennisguse.opentracks.fragments.IntervalsFragment;
|
||||||
@@ -57,18 +55,16 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
private SharedPreferences sharedPreferences;
|
private SharedPreferences sharedPreferences;
|
||||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||||
private TrackDataHub trackDataHub;
|
private TrackDataHub trackDataHub;
|
||||||
private ViewPager pager;
|
|
||||||
private TrackController trackController;
|
private TrackController trackController;
|
||||||
|
|
||||||
|
private TrackRecordingBinding viewBinding;
|
||||||
|
|
||||||
// Initialized from Intent; if a new track recording is started, a new TrackId will be provided by TrackRecordingService
|
// Initialized from Intent; if a new track recording is started, a new TrackId will be provided by TrackRecordingService
|
||||||
private Track.Id trackId;
|
private Track.Id trackId;
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
private boolean recordingTrackPaused;
|
private boolean recordingTrackPaused;
|
||||||
|
|
||||||
// Intervals recording fragment needs Track.Id when the activity creates it.
|
|
||||||
private OnTrackIdListener intervalsListener;
|
|
||||||
|
|
||||||
private final Runnable bindChangedCallback = new Runnable() {
|
private final Runnable bindChangedCallback = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -85,9 +81,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
if (trackId == null) {
|
if (trackId == null) {
|
||||||
// trackId isn't initialized -> leads a new recording.
|
// trackId isn't initialized -> leads a new recording.
|
||||||
trackId = service.startNewTrack();
|
trackId = service.startNewTrack();
|
||||||
if (intervalsListener != null) {
|
|
||||||
intervalsListener.onTrackId(trackId);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// trackId is initialized -> resumes the track.
|
// trackId is initialized -> resumes the track.
|
||||||
service.resumeTrack(trackId);
|
service.resumeTrack(trackId);
|
||||||
@@ -167,7 +160,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
contentProviderUtils = new ContentProviderUtils(this);
|
contentProviderUtils = new ContentProviderUtils(this);
|
||||||
|
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
||||||
trackId = null;
|
trackId = null;
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
//Activity was recreated.
|
//Activity was recreated.
|
||||||
@@ -182,20 +175,17 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
}
|
}
|
||||||
|
|
||||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
|
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
|
||||||
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
|
||||||
|
|
||||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||||
trackDataHub = new TrackDataHub(this);
|
trackDataHub = new TrackDataHub(this);
|
||||||
|
|
||||||
pager = findViewById(R.id.track_detail_activity_view_pager);
|
viewBinding.trackDetailActivityViewPager.setAdapter(new CustomFragmentPagerAdapter(getSupportFragmentManager(), FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT));
|
||||||
pager.setAdapter(new CustomFragmentPagerAdapter(getSupportFragmentManager(), FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT));
|
viewBinding.trackDetailActivityTablayout.setupWithViewPager(viewBinding.trackDetailActivityViewPager);
|
||||||
TabLayout tabs = findViewById(R.id.track_detail_activity_tablayout);
|
|
||||||
tabs.setupWithViewPager(pager);
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
pager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
|
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
|
||||||
}
|
}
|
||||||
|
|
||||||
trackController = new TrackController(this, trackRecordingServiceConnection, false, recordListener, stopListener);
|
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, false, recordListener, stopListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -273,7 +263,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putInt(CURRENT_TAB_TAG_KEY, pager.getCurrentItem());
|
outState.putInt(CURRENT_TAB_TAG_KEY, viewBinding.trackDetailActivityViewPager.getCurrentItem());
|
||||||
outState.putParcelable(EXTRA_TRACK_ID, trackId);
|
outState.putParcelable(EXTRA_TRACK_ID, trackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,8 +282,9 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getLayoutResId() {
|
protected View getRootView() {
|
||||||
return R.layout.track_record;
|
viewBinding = TrackRecordingBinding.inflate(getLayoutInflater());
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -390,7 +381,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
case 0:
|
case 0:
|
||||||
return StatisticsRecordingFragment.newInstance();
|
return StatisticsRecordingFragment.newInstance();
|
||||||
case 1:
|
case 1:
|
||||||
return IntervalsFragment.IntervalsRecordingFragment.newInstance(trackId);
|
return IntervalsFragment.IntervalsRecordingFragment.newInstance();
|
||||||
case 2:
|
case 2:
|
||||||
return ChartFragment.newInstance(false);
|
return ChartFragment.newInstance(false);
|
||||||
case 3:
|
case 3:
|
||||||
@@ -416,15 +407,4 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface OnTrackIdListener {
|
|
||||||
void onTrackId(Track.Id trackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTrackIdListener(OnTrackIdListener listener) {
|
|
||||||
this.intervalsListener = listener;
|
|
||||||
if (trackId != null) {
|
|
||||||
listener.onTrackId(trackId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -23,10 +23,12 @@ public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.I
|
|||||||
private StackMode stackMode;
|
private StackMode stackMode;
|
||||||
private boolean metricUnits;
|
private boolean metricUnits;
|
||||||
private float sumDistance_m;
|
private float sumDistance_m;
|
||||||
|
private String category;
|
||||||
|
|
||||||
public IntervalStatisticsAdapter(Context context, List<IntervalStatistics.Interval> intervalList, StackMode stackMode) {
|
public IntervalStatisticsAdapter(Context context, List<IntervalStatistics.Interval> intervalList, String category, StackMode stackMode) {
|
||||||
super(context, R.layout.interval_stats_list_item, intervalList);
|
super(context, R.layout.interval_stats_list_item, intervalList);
|
||||||
metricUnits = PreferencesUtils.isMetricUnits(context);
|
metricUnits = PreferencesUtils.isMetricUnits(context);
|
||||||
|
this.category = category;
|
||||||
this.stackMode = stackMode;
|
this.stackMode = stackMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,8 +45,8 @@ public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.I
|
|||||||
intervalView = LayoutInflater.from(getContext()).inflate(R.layout.interval_stats_list_item, parent, false);
|
intervalView = LayoutInflater.from(getContext()).inflate(R.layout.interval_stats_list_item, parent, false);
|
||||||
|
|
||||||
viewHolder.distance = intervalView.findViewById(R.id.interval_item_distance);
|
viewHolder.distance = intervalView.findViewById(R.id.interval_item_distance);
|
||||||
viewHolder.speed = intervalView.findViewById(R.id.interval_item_speed);
|
viewHolder.rate = intervalView.findViewById(R.id.interval_item_rate);
|
||||||
viewHolder.pace = intervalView.findViewById(R.id.interval_item_pace);
|
viewHolder.gain = intervalView.findViewById(R.id.interval_item_gain);
|
||||||
|
|
||||||
intervalView.setTag(viewHolder);
|
intervalView.setTag(viewHolder);
|
||||||
} else {
|
} else {
|
||||||
@@ -58,11 +60,16 @@ public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.I
|
|||||||
}
|
}
|
||||||
viewHolder.distance.setText(StringUtils.formatDistance(getContext(), sumDistance_m, metricUnits));
|
viewHolder.distance.setText(StringUtils.formatDistance(getContext(), sumDistance_m, metricUnits));
|
||||||
|
|
||||||
Pair<String, String> speedParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed_ms(), metricUnits, true);
|
if (PreferencesUtils.isReportSpeed(getContext(), category)) {
|
||||||
viewHolder.speed.setText(speedParts.first + " " + speedParts.second);
|
Pair<String, String> speedParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed_ms(), metricUnits, true);
|
||||||
|
viewHolder.rate.setText(speedParts.first + " " + speedParts.second);
|
||||||
|
} else {
|
||||||
|
Pair<String, String> paceParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed_ms(), metricUnits, false);
|
||||||
|
viewHolder.rate.setText(paceParts.first + " " + paceParts.second);
|
||||||
|
}
|
||||||
|
|
||||||
Pair<String, String> paceParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed_ms(), metricUnits, false);
|
Pair<String, String> gainParts = StringUtils.formatElevation(getContext(), interval.getGain_m(), metricUnits);
|
||||||
viewHolder.pace.setText(paceParts.first + " " + paceParts.second);
|
viewHolder.gain.setText(gainParts.first + " " + gainParts.second);
|
||||||
|
|
||||||
return intervalView;
|
return intervalView;
|
||||||
}
|
}
|
||||||
@@ -77,7 +84,7 @@ public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.I
|
|||||||
|
|
||||||
private static class ViewHolder {
|
private static class ViewHolder {
|
||||||
private TextView distance;
|
private TextView distance;
|
||||||
private TextView speed;
|
private TextView rate;
|
||||||
private TextView pace;
|
private TextView gain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package de.dennisguse.opentracks.android;
|
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps a {@link ContentResolver} and provides access as {@link IContentResolver}.
|
|
||||||
*/
|
|
||||||
public class ContentResolverWrapper implements IContentResolver {
|
|
||||||
|
|
||||||
private final ContentResolver contentResolver;
|
|
||||||
|
|
||||||
public ContentResolverWrapper(ContentResolver contentResolver) {
|
|
||||||
this.contentResolver = contentResolver;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public Uri insert(@NonNull Uri url, @Nullable ContentValues values) {
|
|
||||||
return contentResolver.insert(url, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int bulkInsert(@NonNull Uri url, @NonNull ContentValues[] values) {
|
|
||||||
return contentResolver.bulkInsert(url, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
|
|
||||||
return contentResolver.query(uri, projection, selection, selectionArgs, sortOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String where, @Nullable String[] selectionArgs) {
|
|
||||||
return contentResolver.update(uri, values, where, selectionArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int delete(@NonNull Uri url, @Nullable String where, @Nullable String[] selectionArgs) {
|
|
||||||
return contentResolver.delete(url, where, selectionArgs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package de.dennisguse.opentracks.android;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.RequiresPermission;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines the interface actually shared by {@link android.content.ContentProvider} and {@link android.content.ContentResolver}.
|
|
||||||
* So, both can be used interchangeably.
|
|
||||||
*/
|
|
||||||
public interface IContentResolver {
|
|
||||||
@Nullable
|
|
||||||
Uri insert(@RequiresPermission.Write @NonNull Uri url, @Nullable ContentValues values);
|
|
||||||
|
|
||||||
int bulkInsert(@RequiresPermission.Write @NonNull Uri url, @NonNull ContentValues[] values);
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
Cursor query(@RequiresPermission.Read @NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder);
|
|
||||||
|
|
||||||
int update(@RequiresPermission.Write @NonNull Uri uri, @Nullable ContentValues values, @Nullable String where, @Nullable String[] selectionArgs);
|
|
||||||
|
|
||||||
int delete(@RequiresPermission.Write @NonNull Uri url, @Nullable String where, @Nullable String[] selectionArgs);
|
|
||||||
}
|
|
||||||
@@ -54,17 +54,15 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
|||||||
public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeListener {
|
public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Target number of track points displayed by the map overlay.
|
* Target number of track points displayed by the diagrams (recommended).
|
||||||
* We may display more than this number of points.
|
* We may display more than this number of points.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
|
||||||
private static final int TARGET_DISPLAYED_TRACKPOINTS = 5000;
|
private static final int TARGET_DISPLAYED_TRACKPOINTS = 5000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum number of markers to displayed.
|
* Maximum number of markers to displayed in the diagrams.
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@Deprecated
|
|
||||||
private static final int MAX_DISPLAYED_MARKERS = 128;
|
private static final int MAX_DISPLAYED_MARKERS = 128;
|
||||||
|
|
||||||
private static final String TAG = TrackDataHub.class.getSimpleName();
|
private static final String TAG = TrackDataHub.class.getSimpleName();
|
||||||
|
|||||||
@@ -40,20 +40,28 @@ public final class Marker {
|
|||||||
private String description = "";
|
private String description = "";
|
||||||
private String category = "";
|
private String category = "";
|
||||||
private String icon = "";
|
private String icon = "";
|
||||||
private Track.Id trackId;
|
private final Track.Id trackId;
|
||||||
|
//TODO It is the distance from the track starting point; rename to something more meaningful
|
||||||
private double length = 0.0;
|
private double length = 0.0;
|
||||||
private long duration = 0;
|
private long duration = 0;
|
||||||
private Location location;
|
private final Location location;
|
||||||
@Deprecated //TODO Make an URI instead of String
|
@Deprecated //TODO Make an URI instead of String
|
||||||
private String photoUrl = "";
|
private String photoUrl = "";
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public Marker(@NonNull TrackPoint trackPoint) {
|
public Marker(@NonNull Track.Id trackId, @NonNull TrackPoint trackPoint) {
|
||||||
this.location = trackPoint.getLocation();
|
this(trackId, trackPoint.getLocation());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
//TODO Used by AbstractFileImporter to create an intermediate marker before saving a new one into the database.
|
||||||
public Marker(@NonNull Location location) {
|
public Marker(@NonNull Location location) {
|
||||||
|
this(null, location);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Marker(@NonNull Track.Id trackId, @NonNull Location location) {
|
||||||
this.location = location;
|
this.location = location;
|
||||||
|
this.trackId = trackId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, double length, long duration, @NonNull Location location, String photoUrl) {
|
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, double length, long duration, @NonNull Location location, String photoUrl) {
|
||||||
@@ -112,14 +120,11 @@ public final class Marker {
|
|||||||
this.icon = icon;
|
this.icon = icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Track.Id getTrackId() {
|
public @NonNull
|
||||||
|
Track.Id getTrackId() {
|
||||||
return trackId;
|
return trackId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTrackId(Track.Id trackId) {
|
|
||||||
this.trackId = trackId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getLength() {
|
public double getLength() {
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
|||||||
*/
|
*/
|
||||||
public interface MarkerColumns extends BaseColumns {
|
public interface MarkerColumns extends BaseColumns {
|
||||||
|
|
||||||
String TABLE_NAME = "waypoints";
|
String TABLE_NAME = "markers";
|
||||||
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
|
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
|
||||||
Uri CONTENT_URI_BY_TRACKID = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/trackid");
|
Uri CONTENT_URI_BY_TRACKID = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/trackid");
|
||||||
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint";
|
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint";
|
||||||
@@ -60,7 +60,7 @@ public interface MarkerColumns extends BaseColumns {
|
|||||||
+ DESCRIPTION + " TEXT, "
|
+ DESCRIPTION + " TEXT, "
|
||||||
+ CATEGORY + " TEXT, "
|
+ CATEGORY + " TEXT, "
|
||||||
+ ICON + " TEXT, "
|
+ ICON + " TEXT, "
|
||||||
+ TRACKID + " INTEGER, "
|
+ TRACKID + " INTEGER NOT NULL, "
|
||||||
+ LENGTH + " FLOAT, "
|
+ LENGTH + " FLOAT, "
|
||||||
+ DURATION + " INTEGER, "
|
+ DURATION + " INTEGER, "
|
||||||
+ LONGITUDE + " INTEGER, "
|
+ LONGITUDE + " INTEGER, "
|
||||||
@@ -69,7 +69,8 @@ public interface MarkerColumns extends BaseColumns {
|
|||||||
+ ALTITUDE + " FLOAT, "
|
+ ALTITUDE + " FLOAT, "
|
||||||
+ ACCURACY + " FLOAT, "
|
+ ACCURACY + " FLOAT, "
|
||||||
+ BEARING + " FLOAT, "
|
+ BEARING + " FLOAT, "
|
||||||
+ PHOTOURL + " TEXT"
|
+ PHOTOURL + " TEXT, "
|
||||||
|
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE"
|
||||||
+ ")";
|
+ ")";
|
||||||
|
|
||||||
String CREATE_TABLE_INDEX = "CREATE INDEX " + TABLE_NAME + "_" + TRACKID + "_index ON " + TABLE_NAME + "(" + TRACKID + ")";
|
String CREATE_TABLE_INDEX = "CREATE INDEX " + TABLE_NAME + "_" + TRACKID + "_index ON " + TABLE_NAME + "(" + TRACKID + ")";
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ public class Track {
|
|||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
throw new RuntimeException("Not supported");
|
return String.valueOf(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ public interface TrackPointsColumns extends BaseColumns {
|
|||||||
|
|
||||||
String LONGITUDE = "longitude";
|
String LONGITUDE = "longitude";
|
||||||
String LATITUDE = "latitude";
|
String LATITUDE = "latitude";
|
||||||
|
@Deprecated
|
||||||
double PAUSE_LATITUDE = 100.0;
|
double PAUSE_LATITUDE = 100.0;
|
||||||
|
@Deprecated
|
||||||
double RESUME_LATITUDE = 200.0;
|
double RESUME_LATITUDE = 200.0;
|
||||||
|
|
||||||
String TIME = "time";
|
String TIME = "time";
|
||||||
@@ -55,7 +57,7 @@ public interface TrackPointsColumns extends BaseColumns {
|
|||||||
|
|
||||||
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
|
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||||
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||||
+ TRACKID + " INTEGER, "
|
+ TRACKID + " INTEGER NOT NULL, "
|
||||||
+ LONGITUDE + " INTEGER, "
|
+ LONGITUDE + " INTEGER, "
|
||||||
+ LATITUDE + " INTEGER, "
|
+ LATITUDE + " INTEGER, "
|
||||||
+ TIME + " INTEGER, "
|
+ TIME + " INTEGER, "
|
||||||
@@ -66,7 +68,9 @@ public interface TrackPointsColumns extends BaseColumns {
|
|||||||
+ SENSOR_HEARTRATE + " FLOAT, "
|
+ SENSOR_HEARTRATE + " FLOAT, "
|
||||||
+ SENSOR_CADENCE + " FLOAT, "
|
+ SENSOR_CADENCE + " FLOAT, "
|
||||||
+ SENSOR_POWER + " FLOAT, "
|
+ SENSOR_POWER + " FLOAT, "
|
||||||
+ ELEVATION_GAIN + " FLOAT)";
|
+ ELEVATION_GAIN + " FLOAT, "
|
||||||
|
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE"
|
||||||
|
+ ")";
|
||||||
|
|
||||||
String CREATE_TABLE_INDEX = "CREATE INDEX " + TABLE_NAME + "_" + TRACKID + "_index ON " + TABLE_NAME + "(" + TRACKID + ")";
|
String CREATE_TABLE_INDEX = "CREATE INDEX " + TABLE_NAME + "_" + TRACKID + "_index ON " + TABLE_NAME + "(" + TRACKID + ")";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,12 +35,13 @@ public interface TracksColumns extends BaseColumns {
|
|||||||
String DEFAULT_SORT_ORDER = _ID;
|
String DEFAULT_SORT_ORDER = _ID;
|
||||||
|
|
||||||
// Columns
|
// Columns
|
||||||
String UUID = "uuid";
|
String UUID = "uuid"; // identifier to make tracks globally unique (prevent re-import)
|
||||||
String NAME = "name"; // track name
|
String NAME = "name"; // track name
|
||||||
String DESCRIPTION = "description"; // track description
|
String DESCRIPTION = "description"; // track description
|
||||||
String CATEGORY = "category"; // track activity type
|
String CATEGORY = "category"; // track activity type
|
||||||
String STARTTIME = "starttime"; // track start time
|
String STARTTIME = "starttime"; // track start time
|
||||||
String STOPTIME = "stoptime"; // track stop time
|
String STOPTIME = "stoptime"; // track stop time
|
||||||
|
String MARKER_COUNT = "markerCount"; // the numbers of markers (virtual column)
|
||||||
@Deprecated
|
@Deprecated
|
||||||
String NUMPOINTS = "numpoints"; // number of track points //TODO UNUSED
|
String NUMPOINTS = "numpoints"; // number of track points //TODO UNUSED
|
||||||
String TOTALDISTANCE = "totaldistance"; // total distance
|
String TOTALDISTANCE = "totaldistance"; // total distance
|
||||||
@@ -55,25 +56,25 @@ public interface TracksColumns extends BaseColumns {
|
|||||||
String ELEVATIONGAIN = "elevationgain"; // elevation gain
|
String ELEVATIONGAIN = "elevationgain"; // elevation gain
|
||||||
String ICON = "icon"; // track activity type icon
|
String ICON = "icon"; // track activity type icon
|
||||||
|
|
||||||
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" // table
|
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||||
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " // id
|
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||||
+ NAME + " TEXT, " // name
|
+ NAME + " TEXT, "
|
||||||
+ DESCRIPTION + " TEXT, " // description
|
+ DESCRIPTION + " TEXT, "
|
||||||
+ CATEGORY + " TEXT, " // category
|
+ CATEGORY + " TEXT, "
|
||||||
+ STARTTIME + " INTEGER, " // start time
|
+ STARTTIME + " INTEGER, "
|
||||||
+ STOPTIME + " INTEGER, " // stop time
|
+ STOPTIME + " INTEGER, "
|
||||||
+ NUMPOINTS + " INTEGER, " // num points
|
+ NUMPOINTS + " INTEGER, "
|
||||||
+ TOTALDISTANCE + " FLOAT, " // total distance
|
+ TOTALDISTANCE + " FLOAT, "
|
||||||
+ TOTALTIME + " INTEGER, " // total time
|
+ TOTALTIME + " INTEGER, "
|
||||||
+ MOVINGTIME + " INTEGER, " // moving time
|
+ MOVINGTIME + " INTEGER, "
|
||||||
+ AVGSPEED + " FLOAT, " // average speed
|
+ AVGSPEED + " FLOAT, "
|
||||||
+ AVGMOVINGSPEED + " FLOAT, " // average moving speed
|
+ AVGMOVINGSPEED + " FLOAT, "
|
||||||
+ MAXSPEED + " FLOAT, " // max speed
|
+ MAXSPEED + " FLOAT, "
|
||||||
+ MINELEVATION + " FLOAT, " // min elevation
|
+ MINELEVATION + " FLOAT, "
|
||||||
+ MAXELEVATION + " FLOAT, " // max elevation
|
+ MAXELEVATION + " FLOAT, "
|
||||||
+ ELEVATIONGAIN + " FLOAT, " // elevation gain
|
+ ELEVATIONGAIN + " FLOAT, "
|
||||||
+ ICON + " TEXT, " // icon
|
+ ICON + " TEXT, "
|
||||||
+ UUID + " BLOB)"; // UUID
|
+ UUID + " BLOB)";
|
||||||
|
|
||||||
String CREATE_TABLE_INDEX = "CREATE UNIQUE INDEX " + TABLE_NAME + "_" + UUID + "_index ON " + TABLE_NAME + "(" + UUID + ")";
|
String CREATE_TABLE_INDEX = "CREATE UNIQUE INDEX " + TABLE_NAME + "_" + UUID + "_index ON " + TABLE_NAME + "(" + UUID + ")";
|
||||||
|
|
||||||
|
|||||||
@@ -33,13 +33,12 @@ import androidx.loader.content.Loader;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.BuildConfig;
|
import de.dennisguse.opentracks.BuildConfig;
|
||||||
import de.dennisguse.opentracks.android.ContentResolverWrapper;
|
|
||||||
import de.dennisguse.opentracks.android.IContentResolver;
|
|
||||||
import de.dennisguse.opentracks.content.data.Marker;
|
import de.dennisguse.opentracks.content.data.Marker;
|
||||||
import de.dennisguse.opentracks.content.data.MarkerColumns;
|
import de.dennisguse.opentracks.content.data.MarkerColumns;
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
@@ -48,12 +47,10 @@ import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
|||||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||||
import de.dennisguse.opentracks.util.FileUtils;
|
import de.dennisguse.opentracks.util.FileUtils;
|
||||||
import de.dennisguse.opentracks.util.TrackUtils;
|
|
||||||
import de.dennisguse.opentracks.util.UUIDUtils;
|
import de.dennisguse.opentracks.util.UUIDUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link ContentProviderUtils} implementation.
|
* {@link ContentProviderUtils} implementation.
|
||||||
* Allows to use {@link ContentResolver} and {@link android.content.ContentProvider} interchangeably via {@link IContentResolver}.
|
|
||||||
*
|
*
|
||||||
* @author Leif Hendrik Wilden
|
* @author Leif Hendrik Wilden
|
||||||
*/
|
*/
|
||||||
@@ -73,18 +70,15 @@ public class ContentProviderUtils {
|
|||||||
public static final int MAX_LOADED_MARKERS = 10000;
|
public static final int MAX_LOADED_MARKERS = 10000;
|
||||||
private static final String ID_SEPARATOR = ",";
|
private static final String ID_SEPARATOR = ",";
|
||||||
|
|
||||||
private final IContentResolver contentResolver;
|
private final ContentResolver contentResolver;
|
||||||
private int defaultCursorBatchSize = 2000;
|
private int defaultCursorBatchSize = 2000;
|
||||||
|
|
||||||
public ContentProviderUtils(Context context) {
|
public ContentProviderUtils(Context context) {
|
||||||
this(context.getContentResolver());
|
contentResolver = context.getContentResolver();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
public ContentProviderUtils(ContentResolver contentResolver) {
|
public ContentProviderUtils(ContentResolver contentResolver) {
|
||||||
this.contentResolver = new ContentResolverWrapper(contentResolver);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ContentProviderUtils(IContentResolver contentResolver) {
|
|
||||||
this.contentResolver = contentResolver;
|
this.contentResolver = contentResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +88,7 @@ public class ContentProviderUtils {
|
|||||||
*
|
*
|
||||||
* @param trackId the track id
|
* @param trackId the track id
|
||||||
*/
|
*/
|
||||||
|
@Deprecated //TODO Figure out why we need this.
|
||||||
public void clearTrack(Track.Id trackId) {
|
public void clearTrack(Track.Id trackId) {
|
||||||
deleteTrackPointsAndMarkers(trackId);
|
deleteTrackPointsAndMarkers(trackId);
|
||||||
Track track = new Track();
|
Track track = new Track();
|
||||||
@@ -106,7 +101,7 @@ public class ContentProviderUtils {
|
|||||||
*
|
*
|
||||||
* @param cursor the cursor pointing to the track
|
* @param cursor the cursor pointing to the track
|
||||||
*/
|
*/
|
||||||
public Track createTrack(Cursor cursor) {
|
public static Track createTrack(Cursor cursor) {
|
||||||
int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID);
|
int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID);
|
||||||
int uuidIndex = cursor.getColumnIndexOrThrow(TracksColumns.UUID);
|
int uuidIndex = cursor.getColumnIndexOrThrow(TracksColumns.UUID);
|
||||||
int nameIndex = cursor.getColumnIndexOrThrow(TracksColumns.NAME);
|
int nameIndex = cursor.getColumnIndexOrThrow(TracksColumns.NAME);
|
||||||
@@ -184,17 +179,18 @@ public class ContentProviderUtils {
|
|||||||
FileUtils.deleteDirectoryRecurse(dir);
|
FileUtils.deleteDirectoryRecurse(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void deleteTracks(Context context, @NonNull List<Track.Id> trackIds) {
|
||||||
* Deletes a track.
|
// Delete track folder resources.
|
||||||
*
|
for (Track.Id trackId : trackIds) {
|
||||||
* @param trackId the track id
|
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
|
||||||
*/
|
|
||||||
public void deleteTrack(Context context, Track.Id trackId) {
|
|
||||||
if (trackId == null) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
deleteTrackPointsAndMarkers(trackId);
|
|
||||||
|
|
||||||
|
// Delete track last since it triggers a database vacuum call
|
||||||
|
String whereClause = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
|
||||||
|
contentResolver.delete(TracksColumns.CONTENT_URI, whereClause, trackIds.stream().map(id->Long.toString(id.getId())).toArray(String[]::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteTrack(Context context, @NonNull Track.Id trackId) {
|
||||||
// Delete track folder resources.
|
// Delete track folder resources.
|
||||||
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
|
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
|
||||||
|
|
||||||
@@ -223,14 +219,6 @@ public class ContentProviderUtils {
|
|||||||
return tracks;
|
return tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Loader<Cursor> getTracksCursorLoader(Context context) {
|
|
||||||
String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME,
|
|
||||||
TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME,
|
|
||||||
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON};
|
|
||||||
|
|
||||||
return new CursorLoader(context, TracksColumns.CONTENT_URI, PROJECTION, null, null, TrackUtils.TRACK_SORT_ORDER);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
//TODO Do not use; rather get the proper TrackId (e.g., resume a track might give you some weird effects).
|
//TODO Do not use; rather get the proper TrackId (e.g., resume a track might give you some weird effects).
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@@ -368,7 +356,8 @@ public class ContentProviderUtils {
|
|||||||
location.setBearing(cursor.getFloat(bearingIndex));
|
location.setBearing(cursor.getFloat(bearingIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
Marker marker = new Marker(location);
|
Track.Id trackId = new Track.Id(cursor.getLong(trackIdIndex));
|
||||||
|
Marker marker = new Marker(trackId, location);
|
||||||
|
|
||||||
if (!cursor.isNull(idIndex)) {
|
if (!cursor.isNull(idIndex)) {
|
||||||
marker.setId(new Marker.Id(cursor.getLong(idIndex)));
|
marker.setId(new Marker.Id(cursor.getLong(idIndex)));
|
||||||
@@ -385,9 +374,6 @@ public class ContentProviderUtils {
|
|||||||
if (!cursor.isNull(iconIndex)) {
|
if (!cursor.isNull(iconIndex)) {
|
||||||
marker.setIcon(cursor.getString(iconIndex));
|
marker.setIcon(cursor.getString(iconIndex));
|
||||||
}
|
}
|
||||||
if (!cursor.isNull(trackIdIndex)) {
|
|
||||||
marker.setTrackId(new Track.Id(cursor.getLong(trackIdIndex)));
|
|
||||||
}
|
|
||||||
if (!cursor.isNull(lengthIndex)) {
|
if (!cursor.isNull(lengthIndex)) {
|
||||||
marker.setLength(cursor.getFloat(lengthIndex));
|
marker.setLength(cursor.getFloat(lengthIndex));
|
||||||
}
|
}
|
||||||
@@ -503,7 +489,7 @@ public class ContentProviderUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated //TODO Expose information via Cursor
|
@Deprecated //TODO TracksColumns.MARKER_COUNT while querying for tracks
|
||||||
public int getMarkerCount(Track.Id trackId) {
|
public int getMarkerCount(Track.Id trackId) {
|
||||||
if (!trackId.isValid()) {
|
if (!trackId.isValid()) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -566,9 +552,7 @@ public class ContentProviderUtils {
|
|||||||
values.put(MarkerColumns.DESCRIPTION, marker.getDescription());
|
values.put(MarkerColumns.DESCRIPTION, marker.getDescription());
|
||||||
values.put(MarkerColumns.CATEGORY, marker.getCategory());
|
values.put(MarkerColumns.CATEGORY, marker.getCategory());
|
||||||
values.put(MarkerColumns.ICON, marker.getIcon());
|
values.put(MarkerColumns.ICON, marker.getIcon());
|
||||||
if (marker.getTrackId() != null) {
|
values.put(MarkerColumns.TRACKID, marker.getTrackId().getId());
|
||||||
values.put(MarkerColumns.TRACKID, marker.getTrackId().getId());
|
|
||||||
}
|
|
||||||
values.put(MarkerColumns.LENGTH, marker.getLength());
|
values.put(MarkerColumns.LENGTH, marker.getLength());
|
||||||
values.put(MarkerColumns.DURATION, marker.getDuration());
|
values.put(MarkerColumns.DURATION, marker.getDuration());
|
||||||
|
|
||||||
|
|||||||
@@ -33,12 +33,16 @@ import android.util.Log;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.content.data.MarkerColumns;
|
import de.dennisguse.opentracks.content.data.MarkerColumns;
|
||||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link ContentProvider} that handles access to track points, tracks, and markers tables.
|
* A {@link ContentProvider} that handles access to track points, tracks, and markers tables.
|
||||||
|
* <p>
|
||||||
|
* Data consistency is enforced using Foreign Key Constraints within the database incl. cascading deletes.
|
||||||
*
|
*
|
||||||
* @author Leif Hendrik Wilden
|
* @author Leif Hendrik Wilden
|
||||||
*/
|
*/
|
||||||
@@ -82,6 +86,8 @@ public class CustomContentProvider extends ContentProvider {
|
|||||||
CustomSQLiteOpenHelper databaseHelper = new CustomSQLiteOpenHelper(context);
|
CustomSQLiteOpenHelper databaseHelper = new CustomSQLiteOpenHelper(context);
|
||||||
try {
|
try {
|
||||||
db = databaseHelper.getWritableDatabase();
|
db = databaseHelper.getWritableDatabase();
|
||||||
|
// Necessary to enable cascade deletion from Track to TrackPoints and Markers
|
||||||
|
db.setForeignKeyConstraintsEnabled(true);
|
||||||
} catch (SQLiteException e) {
|
} catch (SQLiteException e) {
|
||||||
Log.e(TAG, "Unable to open database for writing.", e);
|
Log.e(TAG, "Unable to open database for writing.", e);
|
||||||
}
|
}
|
||||||
@@ -206,7 +212,11 @@ public class CustomContentProvider extends ContentProvider {
|
|||||||
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
|
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
|
||||||
break;
|
break;
|
||||||
case TRACKS:
|
case TRACKS:
|
||||||
queryBuilder.setTables(TracksColumns.TABLE_NAME);
|
if (projection != null && Arrays.asList(projection).contains(TracksColumns.MARKER_COUNT)) {
|
||||||
|
queryBuilder.setTables(TracksColumns.TABLE_NAME + " LEFT OUTER JOIN (SELECT " + MarkerColumns.TRACKID + " AS markerTrackId, COUNT(*) AS " + TracksColumns.MARKER_COUNT + " FROM " + MarkerColumns.TABLE_NAME + " GROUP BY " + MarkerColumns.TRACKID + ") ON (" + TracksColumns.TABLE_NAME + "." + TracksColumns._ID + "= markerTrackId)");
|
||||||
|
} else {
|
||||||
|
queryBuilder.setTables(TracksColumns.TABLE_NAME);
|
||||||
|
}
|
||||||
sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER;
|
sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER;
|
||||||
break;
|
break;
|
||||||
case TRACKS_BY_ID:
|
case TRACKS_BY_ID:
|
||||||
@@ -287,11 +297,6 @@ public class CustomContentProvider extends ContentProvider {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the {@link UrlType} for a url.
|
|
||||||
*
|
|
||||||
* @param url the url
|
|
||||||
*/
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private UrlType getUrlType(Uri url) {
|
private UrlType getUrlType(Uri url) {
|
||||||
UrlType[] urlTypes = UrlType.values();
|
UrlType[] urlTypes = UrlType.values();
|
||||||
@@ -323,12 +328,6 @@ public class CustomContentProvider extends ContentProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts a track point.
|
|
||||||
*
|
|
||||||
* @param url the content url
|
|
||||||
* @param values the content values
|
|
||||||
*/
|
|
||||||
private Uri insertTrackPoint(Uri url, ContentValues values) {
|
private Uri insertTrackPoint(Uri url, ContentValues values) {
|
||||||
boolean hasLatitude = values.containsKey(TrackPointsColumns.LATITUDE);
|
boolean hasLatitude = values.containsKey(TrackPointsColumns.LATITUDE);
|
||||||
boolean hasLongitude = values.containsKey(TrackPointsColumns.LONGITUDE);
|
boolean hasLongitude = values.containsKey(TrackPointsColumns.LONGITUDE);
|
||||||
@@ -343,12 +342,6 @@ public class CustomContentProvider extends ContentProvider {
|
|||||||
throw new SQLiteException("Failed to insert a track point " + url);
|
throw new SQLiteException("Failed to insert a track point " + url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts a track.
|
|
||||||
*
|
|
||||||
* @param url the content url
|
|
||||||
* @param contentValues the content values
|
|
||||||
*/
|
|
||||||
private Uri insertTrack(Uri url, ContentValues contentValues) {
|
private Uri insertTrack(Uri url, ContentValues contentValues) {
|
||||||
boolean hasStartTime = contentValues.containsKey(TracksColumns.STARTTIME);
|
boolean hasStartTime = contentValues.containsKey(TracksColumns.STARTTIME);
|
||||||
if (!hasStartTime) {
|
if (!hasStartTime) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
|||||||
|
|
||||||
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
|
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
|
||||||
|
|
||||||
private static final int DATABASE_VERSION = 27;
|
private static final int DATABASE_VERSION = 28;
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public static final String DATABASE_NAME = "database.db";
|
public static final String DATABASE_NAME = "database.db";
|
||||||
@@ -73,7 +73,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
|||||||
case 27:
|
case 27:
|
||||||
upgradeFrom26to27(db);
|
upgradeFrom26to27(db);
|
||||||
break;
|
break;
|
||||||
|
case 28:
|
||||||
|
upgradeFrom27to28(db);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
|
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
|
||||||
@@ -98,6 +100,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
|||||||
case 26:
|
case 26:
|
||||||
downgradeFrom27to26(db);
|
downgradeFrom27to26(db);
|
||||||
break;
|
break;
|
||||||
|
case 27:
|
||||||
|
downgradeFrom28to27(db);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
|
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
|
||||||
@@ -142,6 +147,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
|||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add indeces for foreign key trackId
|
||||||
|
*/
|
||||||
private void upgradeFrom24to25(SQLiteDatabase db) {
|
private void upgradeFrom24to25(SQLiteDatabase db) {
|
||||||
db.beginTransaction();
|
db.beginTransaction();
|
||||||
|
|
||||||
@@ -162,6 +170,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
|||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add track UUID to prevent re-import of existing tracks
|
||||||
|
*/
|
||||||
private void upgradeFrom25to26(SQLiteDatabase db) {
|
private void upgradeFrom25to26(SQLiteDatabase db) {
|
||||||
db.beginTransaction();
|
db.beginTransaction();
|
||||||
|
|
||||||
@@ -198,6 +209,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
|||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add elevation gain
|
||||||
|
*/
|
||||||
private void upgradeFrom26to27(SQLiteDatabase db) {
|
private void upgradeFrom26to27(SQLiteDatabase db) {
|
||||||
db.beginTransaction();
|
db.beginTransaction();
|
||||||
|
|
||||||
@@ -220,4 +234,54 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
|||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add foreign key constraints on trackId
|
||||||
|
*/
|
||||||
|
private void upgradeFrom27to28(SQLiteDatabase db) {
|
||||||
|
db.beginTransaction();
|
||||||
|
|
||||||
|
// TrackPoints
|
||||||
|
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
|
||||||
|
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
|
||||||
|
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain FROM trackpoints_old");
|
||||||
|
db.execSQL("DROP TABLE trackpoints_old");
|
||||||
|
|
||||||
|
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
|
||||||
|
|
||||||
|
// Markers
|
||||||
|
db.execSQL("ALTER TABLE waypoints RENAME TO markers_old");
|
||||||
|
db.execSQL("CREATE TABLE markers (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, icon TEXT, trackid INTEGER NOT NULL, length FLOAT, duration INTEGER, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, bearing FLOAT, photoUrl TEXT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
|
||||||
|
|
||||||
|
db.execSQL("INSERT INTO markers SELECT _id, name, description, category, icon, trackid, length, duration, longitude, latitude, time, elevation, accuracy, bearing, photoUrl FROM markers_old");
|
||||||
|
db.execSQL("DROP TABLE markers_old");
|
||||||
|
|
||||||
|
db.execSQL("CREATE INDEX markers_trackid_index ON markers(trackid)");
|
||||||
|
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
db.endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void downgradeFrom28to27(SQLiteDatabase db) {
|
||||||
|
db.beginTransaction();
|
||||||
|
|
||||||
|
// TrackPoints
|
||||||
|
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
|
||||||
|
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT)");
|
||||||
|
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain FROM trackpoints_old");
|
||||||
|
db.execSQL("DROP TABLE trackpoints_old");
|
||||||
|
|
||||||
|
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
|
||||||
|
|
||||||
|
// Markers
|
||||||
|
db.execSQL("CREATE TABLE waypoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, icon TEXT, trackid INTEGER, length FLOAT, duration INTEGER, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, bearing FLOAT, photoUrl TEXT)");
|
||||||
|
|
||||||
|
db.execSQL("INSERT INTO waypoints SELECT _id, name, description, category, icon, trackid, length, duration, longitude, latitude, time, elevation, accuracy, bearing, photoUrl FROM markers");
|
||||||
|
db.execSQL("DROP TABLE markers");
|
||||||
|
|
||||||
|
db.execSQL("CREATE INDEX waypoints_trackid_index ON waypoints(trackid)");
|
||||||
|
|
||||||
|
db.setTransactionSuccessful();
|
||||||
|
db.endTransaction();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.database.MatrixCursor;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
@@ -19,11 +20,11 @@ import java.io.FileOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.android.IContentResolver;
|
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||||
@@ -41,7 +42,7 @@ import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
|||||||
* While handling a request {@link ShareContentProvider} could `grantPermissions()` to the calling app for {@link CustomContentProvider}'s URI.
|
* While handling a request {@link ShareContentProvider} could `grantPermissions()` to the calling app for {@link CustomContentProvider}'s URI.
|
||||||
* However, while handling the request this would allow the calling app to actually contact {@link CustomContentProvider} directly and get access to stored data that should remain private.
|
* However, while handling the request this would allow the calling app to actually contact {@link CustomContentProvider} directly and get access to stored data that should remain private.
|
||||||
*/
|
*/
|
||||||
public class ShareContentProvider extends CustomContentProvider implements IContentResolver {
|
public class ShareContentProvider extends CustomContentProvider {
|
||||||
|
|
||||||
private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
|
private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
|
||||||
|
|
||||||
@@ -209,12 +210,15 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
|
|||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
|
|
||||||
|
|
||||||
Set<Track.Id> trackIds = parseURI(uri);
|
Set<Track.Id> trackIds = parseURI(uri);
|
||||||
final ArrayList<Track> tracks = new ArrayList<>();
|
final ArrayList<Track> tracks = new ArrayList<>();
|
||||||
for (Track.Id trackId : trackIds) {
|
String[] trackIdsString = trackIds.stream().map(Track.Id::toString).toArray(String[]::new);
|
||||||
tracks.add(contentProviderUtils.getTrack(trackId));
|
String whereClause = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
|
||||||
|
|
||||||
|
try (Cursor cursor = super.query(TracksColumns.CONTENT_URI, null, whereClause, trackIdsString, TracksColumns._ID)) {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
tracks.add(ContentProviderUtils.createTrack(cursor));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final TrackExporter trackExporter = getTrackFileFormat(uri).newTrackExporter(getContext());
|
final TrackExporter trackExporter = getTrackFileFormat(uri).newTrackExporter(getContext());
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package de.dennisguse.opentracks.content.sensor;
|
package de.dennisguse.opentracks.content.sensor;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
|
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
|
||||||
@@ -11,6 +13,10 @@ public class SensorData {
|
|||||||
|
|
||||||
private final long timestamp_ms;
|
private final long timestamp_ms;
|
||||||
|
|
||||||
|
SensorData(String sensorAddress) {
|
||||||
|
this(sensorAddress, null, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
SensorData(String sensorAddress, String sensorName) {
|
SensorData(String sensorAddress, String sensorName) {
|
||||||
this(sensorAddress, sensorName, System.currentTimeMillis());
|
this(sensorAddress, sensorName, System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
@@ -22,14 +28,20 @@ public class SensorData {
|
|||||||
this.timestamp_ms = timestamp_ms;
|
this.timestamp_ms = timestamp_ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public String getSensorAddress() {
|
public String getSensorAddress() {
|
||||||
return sensorAddress;
|
return sensorAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
public String getSensorName() {
|
public String getSensorName() {
|
||||||
return sensorName;
|
return sensorName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSensorNameOrAddress() {
|
||||||
|
return sensorName != null ? sensorName : sensorAddress;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the data recent considering the current time.
|
* Is the data recent considering the current time.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,17 +17,20 @@ public final class SensorDataCycling {
|
|||||||
|
|
||||||
private static final String TAG = SensorDataCycling.class.getSimpleName();
|
private static final String TAG = SensorDataCycling.class.getSimpleName();
|
||||||
|
|
||||||
private static final int INVALID_VALUE_INT = -1;
|
|
||||||
private static final float INVALID_VALUE_FLOAT = Float.NaN;
|
|
||||||
|
|
||||||
private SensorDataCycling() {
|
private SensorDataCycling() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Cadence extends SensorData {
|
public static class Cadence extends SensorData {
|
||||||
|
|
||||||
private final long crankRevolutionsCount; // UINT32
|
private final Long crankRevolutionsCount; // UINT32
|
||||||
private final int crankRevolutionsTime; // UINT16; 1/1024s
|
private final Integer crankRevolutionsTime; // UINT16; 1/1024s
|
||||||
private float cadence_rpm = INVALID_VALUE_FLOAT;
|
private Float cadence_rpm;
|
||||||
|
|
||||||
|
public Cadence(String sensorAddress) {
|
||||||
|
super(sensorAddress);
|
||||||
|
this.crankRevolutionsCount = null;
|
||||||
|
this.crankRevolutionsTime = null;
|
||||||
|
}
|
||||||
|
|
||||||
public Cadence(String sensorAddress, String sensorName, long crankRevolutionsCount, int crankRevolutionsTime) {
|
public Cadence(String sensorAddress, String sensorName, long crankRevolutionsCount, int crankRevolutionsTime) {
|
||||||
super(sensorAddress, sensorName);
|
super(sensorAddress, sensorName);
|
||||||
@@ -43,7 +46,7 @@ public final class SensorDataCycling {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasData() {
|
public boolean hasData() {
|
||||||
return crankRevolutionsCount != INVALID_VALUE_INT && crankRevolutionsTime != INVALID_VALUE_INT;
|
return crankRevolutionsCount != null && crankRevolutionsTime != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getCrankRevolutionsCount() {
|
public long getCrankRevolutionsCount() {
|
||||||
@@ -55,7 +58,7 @@ public final class SensorDataCycling {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasCadence_rpm() {
|
public boolean hasCadence_rpm() {
|
||||||
return !Float.isNaN(cadence_rpm);
|
return cadence_rpm != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getCadence_rpm() {
|
public float getCadence_rpm() {
|
||||||
@@ -67,7 +70,7 @@ public final class SensorDataCycling {
|
|||||||
float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
|
float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
|
||||||
if (timeDiff_ms <= 0) {
|
if (timeDiff_ms <= 0) {
|
||||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
||||||
cadence_rpm = INVALID_VALUE_FLOAT;
|
cadence_rpm = null;
|
||||||
} else {
|
} else {
|
||||||
long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX);
|
long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX);
|
||||||
float cadence_ms = crankDiff / timeDiff_ms;
|
float cadence_ms = crankDiff / timeDiff_ms;
|
||||||
@@ -93,9 +96,15 @@ public final class SensorDataCycling {
|
|||||||
|
|
||||||
public static class Speed extends SensorData {
|
public static class Speed extends SensorData {
|
||||||
|
|
||||||
private final int wheelRevolutionsCount; // UINT16
|
private final Integer wheelRevolutionsCount; // UINT16
|
||||||
private final int wheelRevolutionsTime; // UINT16; 1/1024s
|
private final Integer wheelRevolutionsTime; // UINT16; 1/1024s
|
||||||
private float speed_mps = INVALID_VALUE_FLOAT;
|
private Float speed_mps;
|
||||||
|
|
||||||
|
public Speed(String sensorAddress) {
|
||||||
|
super(sensorAddress);
|
||||||
|
this.wheelRevolutionsCount = null;
|
||||||
|
this.wheelRevolutionsTime = null;
|
||||||
|
}
|
||||||
|
|
||||||
public Speed(String sensorAddress, String sensorName, int wheelRevolutionsCount, int wheelRevolutionsTime) {
|
public Speed(String sensorAddress, String sensorName, int wheelRevolutionsCount, int wheelRevolutionsTime) {
|
||||||
super(sensorAddress, sensorName);
|
super(sensorAddress, sensorName);
|
||||||
@@ -104,7 +113,7 @@ public final class SensorDataCycling {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasData() {
|
public boolean hasData() {
|
||||||
return wheelRevolutionsCount != INVALID_VALUE_INT && wheelRevolutionsTime != INVALID_VALUE_INT;
|
return wheelRevolutionsCount != null && wheelRevolutionsTime != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getWheelRevolutionsCount() {
|
public int getWheelRevolutionsCount() {
|
||||||
@@ -116,7 +125,7 @@ public final class SensorDataCycling {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasSpeed_mps() {
|
public boolean hasSpeed_mps() {
|
||||||
return !Float.isNaN(speed_mps);
|
return speed_mps != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getSpeed_mps() {
|
public float getSpeed_mps() {
|
||||||
@@ -128,7 +137,7 @@ public final class SensorDataCycling {
|
|||||||
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
|
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
|
||||||
if (timeDiff_ms <= 0) {
|
if (timeDiff_ms <= 0) {
|
||||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
||||||
speed_mps = INVALID_VALUE_FLOAT;
|
speed_mps = null;
|
||||||
} else {
|
} else {
|
||||||
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
|
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
|
||||||
double timeDiff_s = timeDiff_ms * UnitConversions.MS_TO_S;
|
double timeDiff_s = timeDiff_ms * UnitConversions.MS_TO_S;
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
public class SensorDataHeartRate extends SensorData {
|
public class SensorDataHeartRate extends SensorData {
|
||||||
|
|
||||||
private final float heartRate_bpm;
|
private final Float heartRate_bpm;
|
||||||
|
|
||||||
|
public SensorDataHeartRate(String address) {
|
||||||
|
super(address);
|
||||||
|
heartRate_bpm = null;
|
||||||
|
}
|
||||||
|
|
||||||
public SensorDataHeartRate(String name, String address, float heartRate_bpm) {
|
public SensorDataHeartRate(String name, String address, float heartRate_bpm) {
|
||||||
super(name, address);
|
super(name, address);
|
||||||
@@ -12,10 +17,10 @@ public class SensorDataHeartRate extends SensorData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasHeartRate_bpm() {
|
public boolean hasHeartRate_bpm() {
|
||||||
return !Float.isNaN(heartRate_bpm);
|
return heartRate_bpm != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getHeartRate_bpm() {
|
public Float getHeartRate_bpm() {
|
||||||
return heartRate_bpm;
|
return heartRate_bpm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,21 +28,11 @@ public final class SensorDataSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void set(SensorData data) {
|
public void set(SensorData data) {
|
||||||
if (data instanceof SensorDataHeartRate) {
|
set(data, data);
|
||||||
this.heartRate = (SensorDataHeartRate) data;
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data instanceof SensorDataCycling.Cadence) {
|
public void remove(SensorData type) {
|
||||||
this.cyclingCadence = (SensorDataCycling.Cadence) data;
|
set(type, null);
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (data instanceof SensorDataCycling.Speed) {
|
|
||||||
this.cyclingSpeed = (SensorDataCycling.Speed) data;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
@@ -72,4 +62,22 @@ public final class SensorDataSet {
|
|||||||
+ (getCyclingCadence() != null ? " " + getCyclingCadence() : "")
|
+ (getCyclingCadence() != null ? " " + getCyclingCadence() : "")
|
||||||
+ (getCyclingSpeed() != null ? " " + getCyclingSpeed() : "");
|
+ (getCyclingSpeed() != null ? " " + getCyclingSpeed() : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void set(SensorData type, SensorData data) {
|
||||||
|
if (type instanceof SensorDataHeartRate) {
|
||||||
|
this.heartRate = (SensorDataHeartRate) data;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type instanceof SensorDataCycling.Cadence) {
|
||||||
|
this.cyclingCadence = (SensorDataCycling.Cadence) data;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type instanceof SensorDataCycling.Speed) {
|
||||||
|
this.cyclingSpeed = (SensorDataCycling.Speed) data;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import de.dennisguse.opentracks.content.TrackDataListener;
|
|||||||
import de.dennisguse.opentracks.content.data.Marker;
|
import de.dennisguse.opentracks.content.data.Marker;
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||||
|
import de.dennisguse.opentracks.databinding.ChartBinding;
|
||||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||||
import de.dennisguse.opentracks.util.LocationUtils;
|
import de.dennisguse.opentracks.util.LocationUtils;
|
||||||
@@ -73,6 +74,8 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
|||||||
private TrackStatisticsUpdater trackStatisticsUpdater;
|
private TrackStatisticsUpdater trackStatisticsUpdater;
|
||||||
private long startTime;
|
private long startTime;
|
||||||
|
|
||||||
|
private ChartBinding viewBinding;
|
||||||
|
|
||||||
//TODO Why is this needed?
|
//TODO Why is this needed?
|
||||||
private int recordingDistanceInterval;
|
private int recordingDistanceInterval;
|
||||||
private String category = "";
|
private String category = "";
|
||||||
@@ -147,15 +150,16 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.chart, container, false);
|
viewBinding = ChartBinding.inflate(inflater, container, false);
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
ViewGroup layout = getView().findViewById(R.id.chart_view_layout);
|
|
||||||
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
||||||
layout.addView(chartView, layoutParams);
|
viewBinding.chartViewLayout.addView(chartView, layoutParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -179,8 +183,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
|||||||
@Override
|
@Override
|
||||||
public void onStop() {
|
public void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
ViewGroup layout = getView().findViewById(R.id.chart_view_layout);
|
|
||||||
layout.removeView(chartView);
|
viewBinding.chartViewLayout.removeView(chartView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
viewBinding = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.view.LayoutInflater;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.GridView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
@@ -17,6 +16,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
|
import de.dennisguse.opentracks.databinding.ChooseActivityTypeBinding;
|
||||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,6 +39,8 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
|
|||||||
return TrackIconUtils.getAllIconValues().indexOf(iconValue);
|
return TrackIconUtils.getAllIconValues().indexOf(iconValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ChooseActivityTypeBinding viewBinding;
|
||||||
|
|
||||||
private final String preselectedCategory;
|
private final String preselectedCategory;
|
||||||
|
|
||||||
private ChooseActivityTypeCaller chooseActivityTypeCaller;
|
private ChooseActivityTypeCaller chooseActivityTypeCaller;
|
||||||
@@ -57,8 +59,7 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
View view = inflater.inflate(R.layout.choose_activity_type, container);
|
viewBinding = ChooseActivityTypeBinding.inflate(inflater, container, false);
|
||||||
GridView gridView = view.findViewById(R.id.choose_activity_type_grid_view);
|
|
||||||
|
|
||||||
List<Integer> imageIds = new ArrayList<>();
|
List<Integer> imageIds = new ArrayList<>();
|
||||||
for (String iconValue : TrackIconUtils.getAllIconValues()) {
|
for (String iconValue : TrackIconUtils.getAllIconValues()) {
|
||||||
@@ -66,15 +67,14 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
final ChooseActivityTypeImageAdapter imageAdapter = new ChooseActivityTypeImageAdapter(imageIds);
|
final ChooseActivityTypeImageAdapter imageAdapter = new ChooseActivityTypeImageAdapter(imageIds);
|
||||||
gridView.setAdapter(imageAdapter);
|
|
||||||
|
|
||||||
int position = getPosition(getContext(), preselectedCategory);
|
int position = getPosition(getContext(), preselectedCategory);
|
||||||
if (position != -1) {
|
if (position != -1) {
|
||||||
imageAdapter.setSelected(position);
|
imageAdapter.setSelected(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
gridView.setOnItemClickListener(this);
|
viewBinding.chooseActivityTypeGridView.setAdapter(imageAdapter);
|
||||||
return view;
|
viewBinding.chooseActivityTypeGridView.setOnItemClickListener(this);
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onAttach(@NonNull Context context) {
|
public void onAttach(@NonNull Context context) {
|
||||||
@@ -86,6 +86,11 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
viewBinding = null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
|
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
|
||||||
|
|||||||
@@ -1,105 +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.fragments;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.fragment.app.DialogFragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
|
||||||
|
|
||||||
public class ExportFinishedDialogFragment extends DialogFragment {
|
|
||||||
|
|
||||||
private static final String TAG = ExportFinishedDialogFragment.class.getSimpleName();
|
|
||||||
|
|
||||||
private static final String EXTRA_DIRECTORY_NAME_KEY = "directory_name";
|
|
||||||
private static final String EXTRA_EXPORTED_TRACK_COUNT = "exported_track_count";
|
|
||||||
private static final String EXTRA_TOTAL_TRACK_COUNT = "";
|
|
||||||
|
|
||||||
private static final String FINISHED_DIALOG_TAG = "finished_dialog_tag";
|
|
||||||
|
|
||||||
private DismissCallback caller;
|
|
||||||
|
|
||||||
public static void showDialog(FragmentManager fragmentManager, String directoryDisplayName, int successCount, int fileCount) {
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putString(EXTRA_DIRECTORY_NAME_KEY, directoryDisplayName);
|
|
||||||
bundle.putInt(EXTRA_EXPORTED_TRACK_COUNT, successCount);
|
|
||||||
bundle.putInt(EXTRA_TOTAL_TRACK_COUNT, fileCount);
|
|
||||||
|
|
||||||
ExportFinishedDialogFragment dialogFragment = new ExportFinishedDialogFragment();
|
|
||||||
dialogFragment.setArguments(bundle);
|
|
||||||
dialogFragment.show(fragmentManager, FINISHED_DIALOG_TAG);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(@NonNull Context context) {
|
|
||||||
super.onAttach(context);
|
|
||||||
try {
|
|
||||||
caller = (DismissCallback) context;
|
|
||||||
} catch (ClassCastException e) {
|
|
||||||
throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
String directoryDisplayName = getArguments().getString(EXTRA_DIRECTORY_NAME_KEY);
|
|
||||||
int exportedTracksCount = getArguments().getInt(EXTRA_EXPORTED_TRACK_COUNT);
|
|
||||||
int totalTracksCount = getArguments().getInt(EXTRA_TOTAL_TRACK_COUNT);
|
|
||||||
|
|
||||||
int iconId;
|
|
||||||
int titleId;
|
|
||||||
String message;
|
|
||||||
String totalTracks = getResources().getQuantityString(R.plurals.tracks, totalTracksCount, totalTracksCount);
|
|
||||||
if (exportedTracksCount == totalTracksCount) {
|
|
||||||
iconId = R.drawable.ic_dialog_success_24dp;
|
|
||||||
titleId = R.string.generic_success_title;
|
|
||||||
message = getString(
|
|
||||||
R.string.export_success, totalTracks, directoryDisplayName);
|
|
||||||
} else {
|
|
||||||
iconId = R.drawable.ic_dialog_error_24dp;
|
|
||||||
titleId = R.string.generic_error_title;
|
|
||||||
message = getString(R.string.export_error, exportedTracksCount, totalTracks, directoryDisplayName);
|
|
||||||
}
|
|
||||||
Dialog alertDialog = new AlertDialog.Builder(getContext()).setCancelable(true)
|
|
||||||
.setIcon(iconId).setMessage(message)
|
|
||||||
.setPositiveButton(R.string.generic_ok, (dialog, arg1) -> {
|
|
||||||
dialog.dismiss();
|
|
||||||
caller.onDismissed();
|
|
||||||
}).setTitle(titleId)
|
|
||||||
.create();
|
|
||||||
|
|
||||||
alertDialog.setCanceledOnTouchOutside(false);
|
|
||||||
return alertDialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDetach() {
|
|
||||||
super.onDetach();
|
|
||||||
caller = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface DismissCallback {
|
|
||||||
void onDismissed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,245 +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.fragments;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
|
||||||
import androidx.fragment.app.DialogFragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
|
||||||
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.io.file.ExportFile;
|
|
||||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
|
||||||
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
|
||||||
import de.dennisguse.opentracks.util.DialogUtils;
|
|
||||||
import de.dennisguse.opentracks.util.FileUtils;
|
|
||||||
|
|
||||||
public class ExportProgressDialogFragment extends DialogFragment {
|
|
||||||
|
|
||||||
private static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
|
|
||||||
private static final String EXTRA_TRACKFILEFORMAT_KEY = "trackfileformat";
|
|
||||||
|
|
||||||
private static final String TAG = ExportProgressDialogFragment.class.getSimpleName();
|
|
||||||
|
|
||||||
public static final String EXPORT_DIALOG_TAG = "export_dialog_tag";
|
|
||||||
|
|
||||||
private DismissCallback caller;
|
|
||||||
|
|
||||||
private ExportThread exportThread;
|
|
||||||
|
|
||||||
private String directoryDisplayName;
|
|
||||||
|
|
||||||
private int trackExportSuccessCount;
|
|
||||||
private int trackCount;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new instance.
|
|
||||||
*/
|
|
||||||
public static void showDialog(FragmentManager fragmentManager, TrackFileFormat trackFileFormat, Uri documentFileUri) {
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putParcelable(EXTRA_DIRECTORY_URI_KEY, documentFileUri);
|
|
||||||
bundle.putSerializable(EXTRA_TRACKFILEFORMAT_KEY, trackFileFormat);
|
|
||||||
|
|
||||||
ExportProgressDialogFragment dialogFragment = new ExportProgressDialogFragment();
|
|
||||||
dialogFragment.setArguments(bundle);
|
|
||||||
dialogFragment.setRetainInstance(true);
|
|
||||||
dialogFragment.show(fragmentManager, EXPORT_DIALOG_TAG);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
// This fragment uses a background thread, so it should not be recreated during activity re-creation.
|
|
||||||
setRetainInstance(true);
|
|
||||||
|
|
||||||
Uri directoryUri = getArguments().getParcelable(EXTRA_DIRECTORY_URI_KEY);
|
|
||||||
TrackFileFormat trackFileFormat = (TrackFileFormat) getArguments().getSerializable(EXTRA_TRACKFILEFORMAT_KEY);
|
|
||||||
DocumentFile documentFile = DocumentFile.fromTreeUri(getContext(), directoryUri);
|
|
||||||
|
|
||||||
directoryDisplayName = FileUtils.getPath(documentFile);
|
|
||||||
|
|
||||||
exportThread = new ExportThread(trackFileFormat, documentFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(@NonNull Context context) {
|
|
||||||
super.onAttach(context);
|
|
||||||
try {
|
|
||||||
caller = (DismissCallback) context;
|
|
||||||
} catch (ClassCastException e) {
|
|
||||||
throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
ProgressDialog progressDialog = DialogUtils.createHorizontalProgressDialog(getContext(),
|
|
||||||
R.string.export_progress_message,
|
|
||||||
dialog -> {
|
|
||||||
exportThread.interrupt();
|
|
||||||
dialog.dismiss();
|
|
||||||
caller.onExportCanceled(directoryDisplayName, trackExportSuccessCount, trackCount);
|
|
||||||
}, directoryDisplayName);
|
|
||||||
|
|
||||||
progressDialog.setIndeterminate(false);
|
|
||||||
progressDialog.setMax(trackCount);
|
|
||||||
progressDialog.setProgress(Math.min(trackExportSuccessCount, trackCount));
|
|
||||||
|
|
||||||
return progressDialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
setProgress();
|
|
||||||
if (!exportThread.isAlive()) {
|
|
||||||
exportThread.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
if (!exportThread.isAlive()) {
|
|
||||||
exportThread.interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDetach() {
|
|
||||||
super.onDetach();
|
|
||||||
caller = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onProgress(int trackImportSuccessCount, int fileCount) {
|
|
||||||
this.trackExportSuccessCount = trackImportSuccessCount;
|
|
||||||
this.trackCount = fileCount;
|
|
||||||
setProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setProgress() {
|
|
||||||
ProgressDialog dialog = (ProgressDialog) getDialog();
|
|
||||||
if (dialog != null) {
|
|
||||||
dialog.setIndeterminate(false);
|
|
||||||
dialog.setMax(trackCount);
|
|
||||||
dialog.setProgress(Math.min(trackExportSuccessCount, trackCount));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onExportCompleted(int successCount, int fileCount) {
|
|
||||||
if (caller != null && !isDetached()) {
|
|
||||||
caller.onExportFinished(directoryDisplayName, successCount, fileCount);
|
|
||||||
}
|
|
||||||
dismissAllowingStateLoss();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ExportThread extends Thread {
|
|
||||||
|
|
||||||
private final TrackFileFormat trackFileFormat;
|
|
||||||
|
|
||||||
private final DocumentFile directory;
|
|
||||||
|
|
||||||
ExportThread(TrackFileFormat trackFileFormat, DocumentFile directory) {
|
|
||||||
this.directory = directory;
|
|
||||||
this.trackFileFormat = trackFileFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Context context = ExportProgressDialogFragment.this.getContext();
|
|
||||||
|
|
||||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
|
||||||
|
|
||||||
try (Cursor cursor = contentProviderUtils.getTrackCursor(null, null, TracksColumns._ID)) {
|
|
||||||
if (cursor == null) {
|
|
||||||
onExportCompleted(0, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
trackCount = cursor.getCount();
|
|
||||||
for (int i = 0; i < trackCount; i++) {
|
|
||||||
onProgress(i, trackCount);
|
|
||||||
if (Thread.interrupted()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor.moveToPosition(i);
|
|
||||||
Track track = contentProviderUtils.createTrack(cursor);
|
|
||||||
if (track != null && exportTrack(context, track)) {
|
|
||||||
trackExportSuccessCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onExportCompleted(trackExportSuccessCount, trackCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Boolean exportTrack(Context context, Track track) {
|
|
||||||
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context);
|
|
||||||
|
|
||||||
DocumentFile exportDocumentFile = ExportFile.getExportDocumentFile(
|
|
||||||
track.getId(),
|
|
||||||
trackFileFormat.getExtension(),
|
|
||||||
directory,
|
|
||||||
trackFileFormat.getMimeType()
|
|
||||||
);
|
|
||||||
|
|
||||||
try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFile.getUri())) {
|
|
||||||
if (trackExporter.writeTrack(track, outputStream)) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
if (!exportDocumentFile.delete()) {
|
|
||||||
Log.e(TAG, "Unable to delete exportDocumentFile");
|
|
||||||
}
|
|
||||||
Log.e(TAG, "Unable to export track");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
Log.e(TAG, "Unable to open exportDocumentFile " + exportDocumentFile.getName(), e);
|
|
||||||
return false;
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(TAG, "Unable to close exportDocumentFile output stream", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface DismissCallback {
|
|
||||||
void onExportCanceled(String directoryDisplayName, int successCount, int fileCount);
|
|
||||||
|
|
||||||
void onExportFinished(String directoryDisplayName, int successCount, int fileCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +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.fragments;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.fragment.app.DialogFragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
|
||||||
|
|
||||||
public class ImportFinishedDialogFragment extends DialogFragment {
|
|
||||||
|
|
||||||
private static final String TAG = ImportFinishedDialogFragment.class.getSimpleName();
|
|
||||||
|
|
||||||
private static final String EXTRA_DIRECTORY_NAME_KEY = "directory_name";
|
|
||||||
private static final String EXTRA_IMPORTED_TRACK_COUNT = "imported_track_count";
|
|
||||||
private static final String EXTRA_FILE_COUNT = "file_count";
|
|
||||||
|
|
||||||
private static final String FINISHED_DIALOG_TAG = "finished_dialog_tag";
|
|
||||||
|
|
||||||
private DismissCallback caller;
|
|
||||||
|
|
||||||
public static void showDialog(FragmentManager fragmentManager, String directoryDisplayName, int successCount, int fileCount) {
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putString(EXTRA_DIRECTORY_NAME_KEY, directoryDisplayName);
|
|
||||||
bundle.putInt(EXTRA_IMPORTED_TRACK_COUNT, successCount);
|
|
||||||
bundle.putInt(EXTRA_FILE_COUNT, fileCount);
|
|
||||||
|
|
||||||
ImportFinishedDialogFragment dialogFragment = new ImportFinishedDialogFragment();
|
|
||||||
dialogFragment.setArguments(bundle);
|
|
||||||
dialogFragment.show(fragmentManager, FINISHED_DIALOG_TAG);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(@NonNull Context context) {
|
|
||||||
super.onAttach(context);
|
|
||||||
try {
|
|
||||||
caller = (DismissCallback) context;
|
|
||||||
} catch (ClassCastException e) {
|
|
||||||
throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
String directoryDisplayName = getArguments().getString(EXTRA_DIRECTORY_NAME_KEY);
|
|
||||||
int importedTrackCount = getArguments().getInt(EXTRA_IMPORTED_TRACK_COUNT);
|
|
||||||
int totalTrackCount = getArguments().getInt(EXTRA_FILE_COUNT);
|
|
||||||
|
|
||||||
int iconId;
|
|
||||||
int titleId;
|
|
||||||
String message;
|
|
||||||
String totalFiles = getResources().getQuantityString(R.plurals.files, totalTrackCount, totalTrackCount);
|
|
||||||
if (importedTrackCount == totalTrackCount) {
|
|
||||||
if (totalTrackCount == 0) {
|
|
||||||
iconId = R.drawable.ic_dialog_info_24dp;
|
|
||||||
titleId = R.string.import_no_file_title;
|
|
||||||
message = getString(R.string.import_no_file, directoryDisplayName);
|
|
||||||
} else {
|
|
||||||
iconId = R.drawable.ic_dialog_success_24dp;
|
|
||||||
titleId = R.string.generic_success_title;
|
|
||||||
message = getString(R.string.import_success, totalFiles, directoryDisplayName);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
iconId = R.drawable.ic_dialog_error_24dp;
|
|
||||||
titleId = R.string.generic_error_title;
|
|
||||||
message = getString(R.string.import_error, importedTrackCount, totalFiles, directoryDisplayName);
|
|
||||||
}
|
|
||||||
Dialog dialog = new AlertDialog.Builder(getContext())
|
|
||||||
.setCancelable(true).setIcon(iconId)
|
|
||||||
.setMessage(message)
|
|
||||||
.setPositiveButton(R.string.generic_ok, (dialogInterface, which) -> {
|
|
||||||
dialogInterface.dismiss();
|
|
||||||
caller.onDismissed();
|
|
||||||
})
|
|
||||||
.setTitle(titleId)
|
|
||||||
.create();
|
|
||||||
dialog.setCanceledOnTouchOutside(false);
|
|
||||||
return dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDetach() {
|
|
||||||
super.onDetach();
|
|
||||||
caller = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface DismissCallback {
|
|
||||||
void onDismissed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,230 +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.fragments;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
|
||||||
import androidx.fragment.app.DialogFragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
|
||||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
|
||||||
import de.dennisguse.opentracks.io.file.importer.GpxFileTrackImporter;
|
|
||||||
import de.dennisguse.opentracks.io.file.importer.KmlFileTrackImporter;
|
|
||||||
import de.dennisguse.opentracks.io.file.importer.KmzTrackImporter;
|
|
||||||
import de.dennisguse.opentracks.io.file.importer.TrackImporter;
|
|
||||||
import de.dennisguse.opentracks.util.DialogUtils;
|
|
||||||
import de.dennisguse.opentracks.util.FileUtils;
|
|
||||||
|
|
||||||
public class ImportProgressDialogFragment extends DialogFragment {
|
|
||||||
|
|
||||||
private static final String EXTRA_IS_TREE_KEY = "tree_uri";
|
|
||||||
|
|
||||||
private static final String EXTRA_DATA_URI_KEY = "data_uri";
|
|
||||||
|
|
||||||
private static final String TAG = ImportProgressDialogFragment.class.getSimpleName();
|
|
||||||
|
|
||||||
public static final String IMPORT_DIALOG_TAG = "import_dialog_tag";
|
|
||||||
|
|
||||||
private DismissCallback caller;
|
|
||||||
|
|
||||||
private ImportThread importThread;
|
|
||||||
|
|
||||||
private String directoryDisplayName;
|
|
||||||
|
|
||||||
private int trackImportSuccessCount;
|
|
||||||
private int fileCount;
|
|
||||||
|
|
||||||
public static void showDialog(FragmentManager fragmentManager, Uri documentFileUri, boolean isDirectory) {
|
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
bundle.putParcelable(EXTRA_DATA_URI_KEY, documentFileUri);
|
|
||||||
bundle.putBoolean(EXTRA_IS_TREE_KEY, isDirectory);
|
|
||||||
|
|
||||||
ImportProgressDialogFragment dialogFragment = new ImportProgressDialogFragment();
|
|
||||||
dialogFragment.setArguments(bundle);
|
|
||||||
dialogFragment.setRetainInstance(true);
|
|
||||||
dialogFragment.show(fragmentManager, IMPORT_DIALOG_TAG);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
// This fragment uses a background thread, so it should not be recreated during activity re-creation.
|
|
||||||
setRetainInstance(true);
|
|
||||||
|
|
||||||
Uri directoryUri = getArguments().getParcelable(EXTRA_DATA_URI_KEY);
|
|
||||||
DocumentFile documentFile;
|
|
||||||
if (getArguments().getBoolean(EXTRA_IS_TREE_KEY)) {
|
|
||||||
documentFile = DocumentFile.fromTreeUri(getContext(), directoryUri);
|
|
||||||
} else {
|
|
||||||
documentFile = DocumentFile.fromSingleUri(getContext(), directoryUri);
|
|
||||||
}
|
|
||||||
directoryDisplayName = FileUtils.getPath(documentFile);
|
|
||||||
|
|
||||||
importThread = new ImportThread(documentFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(@NonNull Context context) {
|
|
||||||
super.onAttach(context);
|
|
||||||
try {
|
|
||||||
caller = (DismissCallback) context;
|
|
||||||
} catch (ClassCastException e) {
|
|
||||||
throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
ProgressDialog progressDialog = DialogUtils.createHorizontalProgressDialog(
|
|
||||||
getContext(), R.string.import_progress_message,
|
|
||||||
dialogInterface -> {
|
|
||||||
importThread.interrupt();
|
|
||||||
dialogInterface.dismiss();
|
|
||||||
caller.onImportCanceled(directoryDisplayName, trackImportSuccessCount, fileCount);
|
|
||||||
}, directoryDisplayName);
|
|
||||||
|
|
||||||
progressDialog.setIndeterminate(false);
|
|
||||||
progressDialog.setMax(fileCount);
|
|
||||||
progressDialog.setProgress(Math.min(trackImportSuccessCount, fileCount));
|
|
||||||
|
|
||||||
return progressDialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
setProgress();
|
|
||||||
if (!importThread.isAlive()) {
|
|
||||||
importThread.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
if (!importThread.isAlive()) {
|
|
||||||
importThread.interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDetach() {
|
|
||||||
super.onDetach();
|
|
||||||
caller = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onProgress(int trackImportSuccessCount, int fileCount) {
|
|
||||||
this.trackImportSuccessCount = trackImportSuccessCount;
|
|
||||||
this.fileCount = fileCount;
|
|
||||||
setProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setProgress() {
|
|
||||||
ProgressDialog dialog = (ProgressDialog) getDialog();
|
|
||||||
if (dialog != null) {
|
|
||||||
dialog.setIndeterminate(false);
|
|
||||||
dialog.setMax(fileCount);
|
|
||||||
dialog.setProgress(Math.min(trackImportSuccessCount, fileCount));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onImportCompleted(int successCount, int fileCount) {
|
|
||||||
if (caller != null && !isDetached()) {
|
|
||||||
caller.onImportFinished(directoryDisplayName, successCount, fileCount);
|
|
||||||
}
|
|
||||||
dismissAllowingStateLoss();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ImportThread extends Thread {
|
|
||||||
|
|
||||||
private final DocumentFile file;
|
|
||||||
|
|
||||||
ImportThread(DocumentFile file) {
|
|
||||||
this.file = file;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Context context = ImportProgressDialogFragment.this.getContext();
|
|
||||||
|
|
||||||
List<DocumentFile> files = FileUtils.getFiles(file);
|
|
||||||
|
|
||||||
int importedTrackCount = 0;
|
|
||||||
for (int i = 0; i < files.size(); i++) {
|
|
||||||
onProgress(i, files.size());
|
|
||||||
|
|
||||||
if (Thread.interrupted()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (importFile(context, files.get(i))) {
|
|
||||||
importedTrackCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onImportCompleted(importedTrackCount, files.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Imports a file.
|
|
||||||
*
|
|
||||||
* @param file the file
|
|
||||||
*/
|
|
||||||
private boolean importFile(final Context context, final DocumentFile file) {
|
|
||||||
TrackImporter trackImporter;
|
|
||||||
String fileExtension = FileUtils.getExtension(file);
|
|
||||||
|
|
||||||
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
|
|
||||||
trackImporter = new GpxFileTrackImporter(context);
|
|
||||||
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
|
|
||||||
trackImporter = new KmlFileTrackImporter(ImportProgressDialogFragment.this.getContext());
|
|
||||||
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
|
|
||||||
trackImporter = new KmzTrackImporter(context, file.getUri());
|
|
||||||
} else {
|
|
||||||
Log.i(TAG, "Unsupported file format.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) {
|
|
||||||
return trackImporter.importFile(inputStream) != null;
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(TAG, "Unable to import file", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface DismissCallback {
|
|
||||||
void onImportCanceled(String directoryDisplayName, int successCount, int fileCount);
|
|
||||||
|
|
||||||
void onImportFinished(String directoryDisplayName, int successCount, int fileCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,20 +8,22 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.TrackRecordingActivity;
|
import de.dennisguse.opentracks.TrackActivityDataHubInterface;
|
||||||
import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter;
|
import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter;
|
||||||
|
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.Track;
|
||||||
|
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||||
|
import de.dennisguse.opentracks.databinding.IntervalListViewBinding;
|
||||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||||
import de.dennisguse.opentracks.util.UnitConversions;
|
import de.dennisguse.opentracks.util.UnitConversions;
|
||||||
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
|
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
|
||||||
@@ -30,23 +32,22 @@ import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel;
|
|||||||
/**
|
/**
|
||||||
* A fragment to display the intervals from recorded track.
|
* A fragment to display the intervals from recorded track.
|
||||||
*/
|
*/
|
||||||
public class IntervalsFragment extends Fragment {
|
public class IntervalsFragment extends Fragment implements TrackDataListener {
|
||||||
|
|
||||||
private static final String TAG = IntervalsFragment.class.getSimpleName();
|
private static final String TAG = IntervalsFragment.class.getSimpleName();
|
||||||
|
|
||||||
private static final String TRACK_ID_KEY = "trackId";
|
|
||||||
|
|
||||||
private IntervalStatisticsModel viewModel;
|
private IntervalStatisticsModel viewModel;
|
||||||
private ListView intervalListView;
|
|
||||||
protected IntervalStatisticsAdapter.StackMode stackModeListView;
|
protected IntervalStatisticsAdapter.StackMode stackModeListView;
|
||||||
private IntervalStatisticsModel.IntervalOption selectedInterval;
|
private IntervalStatisticsModel.IntervalOption selectedInterval;
|
||||||
|
|
||||||
private String intervalUnit;
|
private String intervalUnit;
|
||||||
private IntervalStatisticsAdapter adapter;
|
private IntervalStatisticsAdapter adapter;
|
||||||
protected Spinner spinnerIntervals;
|
|
||||||
private ArrayAdapter<IntervalStatisticsModel.IntervalOption> spinnerAdapter;
|
private ArrayAdapter<IntervalStatisticsModel.IntervalOption> spinnerAdapter;
|
||||||
|
|
||||||
private Track.Id trackId;
|
private TrackDataHub trackDataHub;
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
private IntervalListViewBinding viewBinding;
|
||||||
|
|
||||||
protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
|
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)) {
|
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||||
@@ -54,23 +55,18 @@ public class IntervalsFragment extends Fragment {
|
|||||||
if (adapter != null) {
|
if (adapter != null) {
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
spinnerAdapter.notifyDataSetChanged();
|
spinnerAdapter.notifyDataSetChanged();
|
||||||
intervalChanged();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Fragment newInstance(Track.Id trackId) {
|
public static Fragment newInstance() {
|
||||||
Bundle bundle = new Bundle();
|
return new IntervalsFragment();
|
||||||
bundle.putParcelable(TRACK_ID_KEY, trackId);
|
|
||||||
|
|
||||||
Fragment fragment = new IntervalsFragment();
|
|
||||||
fragment.setArguments(bundle);
|
|
||||||
return fragment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.interval_list_view, container, false);
|
viewBinding = IntervalListViewBinding.inflate(inflater, container, false);
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -80,20 +76,11 @@ public class IntervalsFragment extends Fragment {
|
|||||||
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
|
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
|
||||||
intervalUnit = PreferencesUtils.isMetricUnits(getContext()) ? getContext().getString(R.string.unit_kilometer) : getContext().getString(R.string.unit_mile);
|
intervalUnit = PreferencesUtils.isMetricUnits(getContext()) ? getContext().getString(R.string.unit_kilometer) : getContext().getString(R.string.unit_mile);
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
|
||||||
trackId = savedInstanceState.getParcelable(TRACK_ID_KEY);
|
|
||||||
} else {
|
|
||||||
trackId = getArguments().getParcelable(TRACK_ID_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
intervalListView = view.findViewById(R.id.interval_list);
|
|
||||||
intervalListView.setEmptyView(view.findViewById(R.id.interval_list_empty_view));
|
|
||||||
|
|
||||||
stackModeListView = IntervalStatisticsAdapter.StackMode.STACK_FROM_TOP;
|
stackModeListView = IntervalStatisticsAdapter.StackMode.STACK_FROM_TOP;
|
||||||
|
|
||||||
viewModel = new ViewModelProvider(this).get(IntervalStatisticsModel.class);
|
viewModel = new IntervalStatisticsModel();
|
||||||
|
|
||||||
spinnerIntervals = view.findViewById(R.id.spinner_intervals);
|
|
||||||
|
|
||||||
spinnerAdapter = new ArrayAdapter<IntervalStatisticsModel.IntervalOption>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
|
spinnerAdapter = new ArrayAdapter<IntervalStatisticsModel.IntervalOption>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
|
||||||
@NonNull
|
@NonNull
|
||||||
@@ -111,22 +98,37 @@ public class IntervalsFragment extends Fragment {
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
spinnerIntervals.setAdapter(spinnerAdapter);
|
|
||||||
|
|
||||||
spinnerIntervals.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
viewBinding.spinnerIntervals.setAdapter(spinnerAdapter);
|
||||||
|
viewBinding.spinnerIntervals.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
|
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
|
||||||
selectedInterval = IntervalStatisticsModel.IntervalOption.values()[i];
|
selectedInterval = IntervalStatisticsModel.IntervalOption.values()[i];
|
||||||
intervalChanged();
|
loadIntervals();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNothingSelected(AdapterView<?> adapterView) {
|
public void onNothingSelected(AdapterView<?> adapterView) {
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
intervalChanged();
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
resumeTrackDataHub();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
pauseTrackDataHub();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
viewBinding = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -135,40 +137,110 @@ public class IntervalsFragment extends Fragment {
|
|||||||
|
|
||||||
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
|
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
|
||||||
|
|
||||||
intervalListView = null;
|
|
||||||
adapter = null;
|
adapter = null;
|
||||||
spinnerIntervals = null;
|
|
||||||
viewModel = null;
|
viewModel = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.putParcelable(TRACK_ID_KEY, trackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update intervals through {@link IntervalStatisticsModel} view model.
|
* Update intervals through {@link IntervalStatisticsModel} view model.
|
||||||
*/
|
*/
|
||||||
public void intervalChanged() {
|
protected synchronized void loadIntervals() {
|
||||||
if (viewModel == null || intervalListView == null) {
|
if (viewModel == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LiveData<IntervalStatistics> liveData = viewModel.getIntervalStats(trackId, selectedInterval);
|
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
|
||||||
liveData.observe(getActivity(), intervalStatistics -> {
|
IntervalStatistics intervalStatistics = viewModel.getIntervalStats(metricUnits, selectedInterval);
|
||||||
if (intervalStatistics != null) {
|
adapter = new IntervalStatisticsAdapter(getContext(), intervalStatistics.getIntervalList(), category, stackModeListView);
|
||||||
adapter = new IntervalStatisticsAdapter(getContext(), intervalStatistics.getIntervalList(), stackModeListView);
|
viewBinding.intervalList.setAdapter(adapter);
|
||||||
intervalListView.setAdapter(adapter);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTrackId(Track.Id trackId) {
|
/**
|
||||||
this.trackId = trackId;
|
* Resumes the trackDataHub.
|
||||||
|
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
|
||||||
|
*/
|
||||||
|
private synchronized void resumeTrackDataHub() {
|
||||||
|
trackDataHub = ((TrackActivityDataHubInterface) getActivity()).getTrackDataHub();
|
||||||
|
trackDataHub.registerTrackDataListener(this, true, false, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class IntervalsRecordingFragment extends IntervalsFragment implements TrackRecordingActivity.OnTrackIdListener {
|
/**
|
||||||
|
* Pauses the trackDataHub.
|
||||||
|
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
|
||||||
|
*/
|
||||||
|
private synchronized void pauseTrackDataHub() {
|
||||||
|
trackDataHub.unregisterTrackDataListener(this);
|
||||||
|
trackDataHub = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTrackUpdated(Track track) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearTrackPoints() {
|
||||||
|
if (isResumed()) {
|
||||||
|
viewModel.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSampledInTrackPoint(TrackPoint trackPoint) {
|
||||||
|
if (isResumed()) {
|
||||||
|
viewModel.add(trackPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
|
||||||
|
if (isResumed()) {
|
||||||
|
viewModel.add(trackPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNewTrackPointsDone() {
|
||||||
|
if (isResumed()) {
|
||||||
|
runOnUiThread(this::loadIntervals);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearMarkers() {
|
||||||
|
// We don't care.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNewMarker(Marker marker) {
|
||||||
|
// We don't care.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNewMarkersDone() {
|
||||||
|
// We don't care.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a runnable on the UI thread.
|
||||||
|
*
|
||||||
|
* @param runnable the runnable
|
||||||
|
*/
|
||||||
|
private void runOnUiThread(Runnable runnable) {
|
||||||
|
FragmentActivity fragmentActivity = getActivity();
|
||||||
|
if (fragmentActivity != null) {
|
||||||
|
fragmentActivity.runOnUiThread(runnable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class IntervalsRecordingFragment extends IntervalsFragment {
|
||||||
// Refreshing intervals stats it's not so demanding so 5 seconds is enough to balance performance and user experience.
|
// Refreshing intervals stats it's not so demanding so 5 seconds is enough to balance performance and user experience.
|
||||||
private static final long UI_UPDATE_INTERVAL = 5 * UnitConversions.ONE_SECOND_MS;
|
private static final long UI_UPDATE_INTERVAL = 5 * UnitConversions.ONE_SECOND_MS;
|
||||||
|
|
||||||
@@ -184,19 +256,13 @@ public class IntervalsFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Fragment newInstance(Track.Id trackId) {
|
public static Fragment newInstance() {
|
||||||
Bundle bundle = new Bundle();
|
return new IntervalsRecordingFragment();
|
||||||
bundle.putParcelable(TRACK_ID_KEY, trackId);
|
|
||||||
|
|
||||||
Fragment fragment = new IntervalsRecordingFragment();
|
|
||||||
fragment.setArguments(bundle);
|
|
||||||
return fragment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
((TrackRecordingActivity) getActivity()).setTrackIdListener(this);
|
|
||||||
intervalHandler = new Handler();
|
intervalHandler = new Handler();
|
||||||
stackModeListView = IntervalStatisticsAdapter.StackMode.STACK_FROM_BOTTOM;
|
stackModeListView = IntervalStatisticsAdapter.StackMode.STACK_FROM_BOTTOM;
|
||||||
}
|
}
|
||||||
@@ -214,12 +280,7 @@ public class IntervalsFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateIntervals() {
|
private void updateIntervals() {
|
||||||
intervalChanged();
|
loadIntervals();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTrackId(Track.Id trackId) {
|
|
||||||
setTrackId(trackId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,9 +30,6 @@ import android.view.ViewGroup;
|
|||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.view.animation.Animation.AnimationListener;
|
import android.view.animation.Animation.AnimationListener;
|
||||||
import android.view.animation.AnimationUtils;
|
import android.view.animation.AnimationUtils;
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
@@ -42,6 +39,7 @@ import de.dennisguse.opentracks.MarkerEditActivity;
|
|||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.content.data.Marker;
|
import de.dennisguse.opentracks.content.data.Marker;
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
|
import de.dennisguse.opentracks.databinding.MarkerDetailFragmentBinding;
|
||||||
import de.dennisguse.opentracks.util.IntentUtils;
|
import de.dennisguse.opentracks.util.IntentUtils;
|
||||||
import de.dennisguse.opentracks.util.ListItemUtils;
|
import de.dennisguse.opentracks.util.ListItemUtils;
|
||||||
import de.dennisguse.opentracks.util.MarkerUtils;
|
import de.dennisguse.opentracks.util.MarkerUtils;
|
||||||
@@ -64,10 +62,10 @@ public class MarkerDetailFragment extends Fragment {
|
|||||||
|
|
||||||
private ContentProviderUtils contentProviderUtils;
|
private ContentProviderUtils contentProviderUtils;
|
||||||
private Handler handler;
|
private Handler handler;
|
||||||
private ImageView photoView;
|
|
||||||
private ImageView textGradient;
|
|
||||||
private LinearLayout markerInfo;
|
|
||||||
private Marker marker;
|
private Marker marker;
|
||||||
|
|
||||||
|
private MarkerDetailFragmentBinding viewBinding;
|
||||||
|
|
||||||
private final Runnable hideText = new Runnable() {
|
private final Runnable hideText = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -84,12 +82,12 @@ public class MarkerDetailFragment extends Fragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAnimationEnd(Animation anim) {
|
public void onAnimationEnd(Animation anim) {
|
||||||
textGradient.setVisibility(View.GONE);
|
viewBinding.markerDetailMarkerTextGradient.setVisibility(View.GONE);
|
||||||
markerInfo.setVisibility(View.GONE);
|
viewBinding.markerDetailMarkerInfo.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
textGradient.startAnimation(animation);
|
viewBinding.markerDetailMarkerTextGradient.startAnimation(animation);
|
||||||
markerInfo.startAnimation(animation);
|
viewBinding.markerDetailMarkerInfo.startAnimation(animation);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,22 +117,18 @@ public class MarkerDetailFragment extends Fragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
View view = inflater.inflate(R.layout.marker_detail_fragment, container, false);
|
viewBinding = MarkerDetailFragmentBinding.inflate(inflater, container, false);
|
||||||
|
|
||||||
photoView = view.findViewById(R.id.marker_detail_marker_photo);
|
viewBinding.markerDetailMarkerPhoto.setOnClickListener(v -> {
|
||||||
textGradient = view.findViewById(R.id.marker_detail_marker_text_gradient);
|
|
||||||
markerInfo = view.findViewById(R.id.marker_detail_marker_info);
|
|
||||||
|
|
||||||
photoView.setOnClickListener(v -> {
|
|
||||||
handler.removeCallbacks(hideText);
|
handler.removeCallbacks(hideText);
|
||||||
int visibility = markerInfo.getVisibility() == View.GONE ? View.VISIBLE : View.GONE;
|
int visibility = viewBinding.markerDetailMarkerInfo.getVisibility() == View.GONE ? View.VISIBLE : View.GONE;
|
||||||
textGradient.setVisibility(visibility);
|
viewBinding.markerDetailMarkerTextGradient.setVisibility(visibility);
|
||||||
markerInfo.setVisibility(visibility);
|
viewBinding.markerDetailMarkerInfo.setVisibility(visibility);
|
||||||
if (visibility == View.VISIBLE) {
|
if (visibility == View.VISIBLE) {
|
||||||
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
|
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return view;
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -153,6 +147,12 @@ public class MarkerDetailFragment extends Fragment {
|
|||||||
handler.removeCallbacks(hideText);
|
handler.removeCallbacks(hideText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
viewBinding = null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setMenuVisibility(boolean menuVisible) {
|
public void setMenuVisibility(boolean menuVisible) {
|
||||||
super.setMenuVisibility(menuVisible);
|
super.setMenuVisibility(menuVisible);
|
||||||
@@ -223,30 +223,23 @@ public class MarkerDetailFragment extends Fragment {
|
|||||||
boolean hasPhoto = marker.hasPhoto();
|
boolean hasPhoto = marker.hasPhoto();
|
||||||
if (hasPhoto) {
|
if (hasPhoto) {
|
||||||
handler.removeCallbacks(hideText);
|
handler.removeCallbacks(hideText);
|
||||||
photoView.setImageURI(marker.getPhotoURI());
|
viewBinding.markerDetailMarkerPhoto.setImageURI(marker.getPhotoURI());
|
||||||
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
|
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
|
||||||
} else {
|
} else {
|
||||||
photoView.setImageResource(MarkerUtils.ICON_ID);
|
viewBinding.markerDetailMarkerPhoto.setImageResource(MarkerUtils.ICON_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
setName(hasPhoto);
|
ListItemUtils.setTextView(getActivity(), viewBinding.markerDetailMarkerName, marker.getName(), hasPhoto);
|
||||||
|
|
||||||
TextView category = getView().findViewById(R.id.marker_detail_marker_category);
|
|
||||||
ListItemUtils.setTextView(getActivity(), category, StringUtils.getCategory(marker.getCategory()), hasPhoto);
|
|
||||||
|
|
||||||
TextView description = getView().findViewById(R.id.marker_detail_marker_description);
|
ListItemUtils.setTextView(getActivity(), viewBinding.markerDetailMarkerCategory, StringUtils.getCategory(marker.getCategory()), hasPhoto);
|
||||||
ListItemUtils.setTextView(getActivity(), description, marker.getDescription(), hasPhoto);
|
|
||||||
|
ListItemUtils.setTextView(getActivity(), viewBinding.markerDetailMarkerDescription, marker.getDescription(), hasPhoto);
|
||||||
|
|
||||||
setLocation(hasPhoto);
|
setLocation(hasPhoto);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setName(boolean addShadow) {
|
|
||||||
TextView textView = getView().findViewById(R.id.marker_detail_marker_name);
|
|
||||||
ListItemUtils.setTextView(getActivity(), textView, marker.getName(), addShadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setLocation(boolean addShadow) {
|
private void setLocation(boolean addShadow) {
|
||||||
TextView textView = getView().findViewById(R.id.marker_detail_marker_location);
|
|
||||||
Location location = marker.getLocation();
|
Location location = marker.getLocation();
|
||||||
String value;
|
String value;
|
||||||
if (location == null) {
|
if (location == null) {
|
||||||
@@ -257,6 +250,6 @@ public class MarkerDetailFragment extends Fragment {
|
|||||||
+ getString(R.string.stats_longitude) + " "
|
+ getString(R.string.stats_longitude) + " "
|
||||||
+ StringUtils.formatCoordinate(location.getLongitude()) + "]";
|
+ StringUtils.formatCoordinate(location.getLongitude()) + "]";
|
||||||
}
|
}
|
||||||
ListItemUtils.setTextView(getActivity(), textView, value, addShadow);
|
ListItemUtils.setTextView(getActivity(), viewBinding.markerDetailMarkerLocation, value, addShadow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,29 +22,28 @@ import android.util.Pair;
|
|||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.constraintlayout.widget.Group;
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
|
import de.dennisguse.opentracks.databinding.StatisticsRecordedBinding;
|
||||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||||
import de.dennisguse.opentracks.util.StringUtils;
|
import de.dennisguse.opentracks.util.StringUtils;
|
||||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A fragment to display track statistics to the user.
|
* A fragment to display track statistics to the user for a recorded {@link Track}.
|
||||||
*
|
*
|
||||||
* @author Sandor Dornbush
|
* @author Sandor Dornbush
|
||||||
* @author Rodrigo Damazio
|
* @author Rodrigo Damazio
|
||||||
*/
|
*/
|
||||||
|
//TODO During updateUI(): do not call PreferenceUtils (it is slow) rather use sharedPreferenceChangeListener.
|
||||||
public class StatisticsRecordedFragment extends Fragment {
|
public class StatisticsRecordedFragment extends Fragment {
|
||||||
|
|
||||||
private static final String TRACK_ID_KEY = "trackId";
|
private static final String TRACK_ID_KEY = "trackId";
|
||||||
@@ -54,6 +53,8 @@ public class StatisticsRecordedFragment extends Fragment {
|
|||||||
private Track.Id trackId;
|
private Track.Id trackId;
|
||||||
private ContentProviderUtils contentProviderUtils;
|
private ContentProviderUtils contentProviderUtils;
|
||||||
|
|
||||||
|
private StatisticsRecordedBinding viewBinding;
|
||||||
|
|
||||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
|
private 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)) {
|
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||||
if (isResumed()) {
|
if (isResumed()) {
|
||||||
@@ -68,29 +69,10 @@ public class StatisticsRecordedFragment extends Fragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.statistics_recorded, container, false);
|
viewBinding = StatisticsRecordedBinding.inflate(inflater, container, false);
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
private TextView totalTimeValueView;
|
|
||||||
private TextView distanceValue;
|
|
||||||
private TextView distanceUnit;
|
|
||||||
private View activityLabel;
|
|
||||||
private ImageView activitySpinner;
|
|
||||||
private TextView movingTimeValue;
|
|
||||||
private TextView speedAvgLabel;
|
|
||||||
private TextView speedAvgValue;
|
|
||||||
private TextView speedAvgUnit;
|
|
||||||
private TextView speedMaxLabel;
|
|
||||||
private TextView speedMaxValue;
|
|
||||||
private TextView speedMaxUnit;
|
|
||||||
private TextView speedMovingLabel;
|
|
||||||
private TextView speedMovingValue;
|
|
||||||
private TextView speedMovingUnit;
|
|
||||||
|
|
||||||
private Group elevationGroup;
|
|
||||||
private TextView elevationTotalGainValue;
|
|
||||||
private TextView elevationTotalGainUnit;
|
|
||||||
|
|
||||||
public static StatisticsRecordedFragment newInstance(Track.Id trackId) {
|
public static StatisticsRecordedFragment newInstance(Track.Id trackId) {
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putParcelable(TRACK_ID_KEY, trackId);
|
bundle.putParcelable(TRACK_ID_KEY, trackId);
|
||||||
@@ -108,37 +90,6 @@ public class StatisticsRecordedFragment extends Fragment {
|
|||||||
contentProviderUtils = new ContentProviderUtils(getContext());
|
contentProviderUtils = new ContentProviderUtils(getContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
|
||||||
super.onViewCreated(view, savedInstanceState);
|
|
||||||
|
|
||||||
totalTimeValueView = view.findViewById(R.id.stats_total_time_value);
|
|
||||||
|
|
||||||
distanceValue = view.findViewById(R.id.stats_distance_value);
|
|
||||||
distanceUnit = view.findViewById(R.id.stats_distance_unit);
|
|
||||||
|
|
||||||
activityLabel = view.findViewById(R.id.stats_activity_type_label);
|
|
||||||
activitySpinner = view.findViewById(R.id.stats_activity_type_icon);
|
|
||||||
|
|
||||||
movingTimeValue = view.findViewById(R.id.stats_moving_time_value);
|
|
||||||
|
|
||||||
speedAvgLabel = view.findViewById(R.id.stats_average_speed_label);
|
|
||||||
speedAvgValue = view.findViewById(R.id.stats_average_speed_value);
|
|
||||||
speedAvgUnit = view.findViewById(R.id.stats_average_speed_unit);
|
|
||||||
|
|
||||||
speedMaxLabel = view.findViewById(R.id.stats_max_speed_label);
|
|
||||||
speedMaxValue = view.findViewById(R.id.stats_max_speed_value);
|
|
||||||
speedMaxUnit = view.findViewById(R.id.stats_max_speed_unit);
|
|
||||||
|
|
||||||
speedMovingLabel = view.findViewById(R.id.stats_moving_speed_label);
|
|
||||||
speedMovingValue = view.findViewById(R.id.stats_moving_speed_value);
|
|
||||||
speedMovingUnit = view.findViewById(R.id.stats_moving_speed_unit);
|
|
||||||
|
|
||||||
elevationGroup = view.findViewById(R.id.stats_elevation_group);
|
|
||||||
elevationTotalGainValue = view.findViewById(R.id.stats_elevation_gain_value);
|
|
||||||
elevationTotalGainUnit = view.findViewById(R.id.stats_elevation_gain_unit);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
@@ -150,34 +101,9 @@ public class StatisticsRecordedFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
|
viewBinding = null;
|
||||||
|
|
||||||
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
|
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
|
||||||
|
|
||||||
totalTimeValueView = null;
|
|
||||||
|
|
||||||
distanceValue = null;
|
|
||||||
distanceUnit = null;
|
|
||||||
|
|
||||||
activityLabel = null;
|
|
||||||
activitySpinner = null;
|
|
||||||
|
|
||||||
movingTimeValue = null;
|
|
||||||
|
|
||||||
speedAvgLabel = null;
|
|
||||||
speedAvgValue = null;
|
|
||||||
speedAvgUnit = null;
|
|
||||||
|
|
||||||
speedMaxLabel = null;
|
|
||||||
speedMaxValue = null;
|
|
||||||
speedMaxUnit = null;
|
|
||||||
|
|
||||||
speedMovingLabel = null;
|
|
||||||
speedMovingValue = null;
|
|
||||||
speedMovingUnit = null;
|
|
||||||
|
|
||||||
elevationGroup = null;
|
|
||||||
elevationTotalGainValue = null;
|
|
||||||
elevationTotalGainUnit = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void loadStatistics() {
|
public void loadStatistics() {
|
||||||
@@ -202,72 +128,66 @@ public class StatisticsRecordedFragment extends Fragment {
|
|||||||
double totalDistance = trackStatistics == null ? Double.NaN : trackStatistics.getTotalDistance();
|
double totalDistance = trackStatistics == null ? Double.NaN : trackStatistics.getTotalDistance();
|
||||||
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
|
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
|
||||||
|
|
||||||
distanceValue.setText(parts.first);
|
viewBinding.statsDistanceValue.setText(parts.first);
|
||||||
distanceUnit.setText(parts.second);
|
viewBinding.statsDistanceUnit.setText(parts.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set activity type
|
// Set activity type
|
||||||
{
|
{
|
||||||
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
|
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
|
||||||
|
viewBinding.statsActivityTypeIcon.setEnabled(false);
|
||||||
activityLabel.setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE);
|
viewBinding.statsActivityTypeIcon.setImageDrawable(ContextCompat.getDrawable(getContext(), TrackIconUtils.getIconDrawable(trackIconValue)));
|
||||||
|
|
||||||
activitySpinner.setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE);
|
|
||||||
activitySpinner.setEnabled(false);
|
|
||||||
if (trackIconValue != null) {
|
|
||||||
activitySpinner.setImageDrawable(ContextCompat.getDrawable(getContext(), TrackIconUtils.getIconDrawable(trackIconValue)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set time and start datetime
|
// Set time and start datetime
|
||||||
if (trackStatistics != null) {
|
if (trackStatistics != null) {
|
||||||
movingTimeValue.setText(StringUtils.formatElapsedTime(trackStatistics.getMovingTime()));
|
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(trackStatistics.getMovingTime()));
|
||||||
totalTimeValueView.setText(StringUtils.formatElapsedTime(trackStatistics.getTotalTime()));
|
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(trackStatistics.getTotalTime()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set average speed/pace
|
// Set average speed/pace
|
||||||
{
|
{
|
||||||
double speed = trackStatistics != null ? trackStatistics.getAverageSpeed() : Double.NaN;
|
double speed = trackStatistics != null ? trackStatistics.getAverageSpeed() : Double.NaN;
|
||||||
speedAvgLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
|
viewBinding.statsAverageSpeedLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
|
||||||
|
|
||||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
||||||
speedAvgValue.setText(parts.first);
|
viewBinding.statsAverageSpeedValue.setText(parts.first);
|
||||||
speedAvgUnit.setText(parts.second);
|
viewBinding.statsAverageSpeedUnit.setText(parts.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set max speed/pace
|
// Set max speed/pace
|
||||||
{
|
{
|
||||||
double speed = trackStatistics == null ? Double.NaN : trackStatistics.getMaxSpeed();
|
double speed = trackStatistics == null ? Double.NaN : trackStatistics.getMaxSpeed();
|
||||||
|
|
||||||
speedMaxLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
|
viewBinding.statsMaxSpeedLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
|
||||||
|
|
||||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
||||||
speedMaxValue.setText(parts.first);
|
viewBinding.statsMaxSpeedValue.setText(parts.first);
|
||||||
speedMaxUnit.setText(parts.second);
|
viewBinding.statsMaxSpeedUnit.setText(parts.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set moving speed/pace
|
// Set moving speed/pace
|
||||||
{
|
{
|
||||||
double speed = trackStatistics != null ? trackStatistics.getAverageMovingSpeed() : Double.NaN;
|
double speed = trackStatistics != null ? trackStatistics.getAverageMovingSpeed() : Double.NaN;
|
||||||
|
|
||||||
speedMovingLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
|
viewBinding.statsMovingSpeedLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
|
||||||
|
|
||||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
||||||
speedMovingValue.setText(parts.first);
|
viewBinding.statsMovingSpeedValue.setText(parts.first);
|
||||||
speedMovingUnit.setText(parts.second);
|
viewBinding.statsMovingSpeedUnit.setText(parts.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set elevation gain
|
// Set elevation gain
|
||||||
{
|
{
|
||||||
// Make elevation visible?
|
// Make elevation visible?
|
||||||
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
|
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
|
||||||
elevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
|
viewBinding.statsElevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
Float elevationGain_m = trackStatistics != null ? trackStatistics.getTotalElevationGain() : null;
|
Float elevationGain_m = trackStatistics != null ? trackStatistics.getTotalElevationGain() : null;
|
||||||
|
|
||||||
Pair<String, String> parts = StringUtils.formatElevation(getContext(), elevationGain_m, metricUnits);
|
Pair<String, String> parts = StringUtils.formatElevation(getContext(), elevationGain_m, metricUnits);
|
||||||
elevationTotalGainValue.setText(parts.first);
|
viewBinding.statsElevationGainValue.setText(parts.first);
|
||||||
elevationTotalGainUnit.setText(parts.second);
|
viewBinding.statsElevationGainUnit.setText(parts.second);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,8 @@ import android.view.LayoutInflater;
|
|||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Spinner;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.constraintlayout.widget.Group;
|
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
@@ -28,6 +24,7 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
|
|||||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||||
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
|
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
|
||||||
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
|
||||||
|
import de.dennisguse.opentracks.databinding.StatisticsRecordingBinding;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||||
@@ -35,15 +32,16 @@ import de.dennisguse.opentracks.util.LocationUtils;
|
|||||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||||
import de.dennisguse.opentracks.util.StringUtils;
|
import de.dennisguse.opentracks.util.StringUtils;
|
||||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||||
import de.dennisguse.opentracks.util.TrackPointUtils;
|
|
||||||
import de.dennisguse.opentracks.util.UnitConversions;
|
import de.dennisguse.opentracks.util.UnitConversions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A fragment to display track statistics to the user.
|
* A fragment to display track statistics to the user for a currently recording {@link Track}.
|
||||||
*
|
*
|
||||||
* @author Sandor Dornbush
|
* @author Sandor Dornbush
|
||||||
* @author Rodrigo Damazio
|
* @author Rodrigo Damazio
|
||||||
*/
|
*/
|
||||||
|
//TODO isRecording should not be relevant anymore as we now have StatisticRecordedFragment.
|
||||||
|
//TODO During updateUI(): do not call PreferenceUtils (it is slow) rather use sharedPreferenceChangeListener.
|
||||||
public class StatisticsRecordingFragment extends Fragment implements TrackDataListener {
|
public class StatisticsRecordingFragment extends Fragment implements TrackDataListener {
|
||||||
|
|
||||||
private static final String TAG = StatisticsRecordingFragment.class.getSimpleName();
|
private static final String TAG = StatisticsRecordingFragment.class.getSimpleName();
|
||||||
@@ -59,53 +57,31 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
private TrackStatistics lastTrackStatistics;
|
private TrackStatistics lastTrackStatistics;
|
||||||
|
|
||||||
private String category = "";
|
private String category = "";
|
||||||
@Deprecated //TODO This should be handled somewhere else; not in the UI.
|
|
||||||
private int recordingGpsAccuracy;
|
private StatisticsRecordingBinding viewBinding;
|
||||||
|
|
||||||
public static Fragment newInstance() {
|
public static Fragment newInstance() {
|
||||||
return new StatisticsRecordingFragment();
|
return new StatisticsRecordingFragment();
|
||||||
}
|
}
|
||||||
|
|
||||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
|
||||||
@Override
|
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
if (isResumed()) {
|
||||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
getActivity().runOnUiThread(() -> {
|
||||||
if (isResumed()) {
|
if (isResumed()) {
|
||||||
getActivity().runOnUiThread(() -> {
|
updateUI();
|
||||||
if (isResumed()) {
|
}
|
||||||
updateUI();
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (PreferencesUtils.isKey(getContext(), R.string.recording_track_id_key, key)) {
|
|
||||||
recordingGpsAccuracy = PreferencesUtils.getRecordingGPSAccuracy(getContext());
|
|
||||||
if (PreferencesUtils.getRecordingTrackId(getContext()).isValid()) {
|
|
||||||
// A recording track id has been set -> Resumes track and starts timer.
|
|
||||||
resumeTrackDataHub();
|
|
||||||
trackRecordingServiceConnection.startConnection(getContext());
|
|
||||||
|
|
||||||
handlerUpdateUI.post(updateUIeachSecond);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.statistics_recording, container, false);
|
viewBinding = StatisticsRecordingBinding.inflate(inflater, container, false);
|
||||||
|
return viewBinding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Views */
|
|
||||||
private View sensorHorizontalLine;
|
|
||||||
private Group heartRateGroup;
|
|
||||||
private TextView heartRateValueView;
|
|
||||||
private TextView heartRateSensorView;
|
|
||||||
private Group cadenceGroup;
|
|
||||||
private TextView cadenceValueView;
|
|
||||||
private TextView cadenceSensorView;
|
|
||||||
|
|
||||||
private TextView totalTimeValueView;
|
|
||||||
private final Runnable updateUIeachSecond = new Runnable() {
|
private final Runnable updateUIeachSecond = new Runnable() {
|
||||||
public void run() {
|
public void run() {
|
||||||
if (isResumed() && isSelectedTrackRecording()) {
|
if (isResumed() && isSelectedTrackRecording()) {
|
||||||
@@ -118,102 +94,21 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
private TextView distanceValue;
|
|
||||||
private TextView distanceUnit;
|
|
||||||
private View activityLabel;
|
|
||||||
private Spinner activitySpinner;
|
|
||||||
private TextView movingTimeValue;
|
|
||||||
private TextView speedAvgLabel;
|
|
||||||
private TextView speedAvgValue;
|
|
||||||
private TextView speedAvgUnit;
|
|
||||||
private TextView speedMaxLabel;
|
|
||||||
private TextView speedMaxValue;
|
|
||||||
private TextView speedMaxUnit;
|
|
||||||
private TextView speedMovingLabel;
|
|
||||||
private TextView speedMovingValue;
|
|
||||||
private TextView speedMovingUnit;
|
|
||||||
private TextView speedLabel;
|
|
||||||
private TextView speedValue;
|
|
||||||
private TextView speedUnit;
|
|
||||||
private Group elevationGroup;
|
|
||||||
private TextView elevationCurrentValue;
|
|
||||||
private TextView elevationCurrentUnit;
|
|
||||||
private TextView elevationTotalGainValue;
|
|
||||||
private TextView elevationTotalGainUnit;
|
|
||||||
private Group coordinateGroup;
|
|
||||||
private TextView latitudeValue;
|
|
||||||
private TextView longitudeValue;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
|
||||||
super.onViewCreated(view, savedInstanceState);
|
|
||||||
|
|
||||||
sensorHorizontalLine = view.findViewById(R.id.stats_sensor_horizontal_line);
|
|
||||||
|
|
||||||
heartRateGroup = view.findViewById(R.id.stats_sensor_heart_rate_group);
|
|
||||||
heartRateValueView = view.findViewById(R.id.stats_sensor_heart_rate_value);
|
|
||||||
heartRateSensorView = view.findViewById(R.id.stats_sensor_heart_rate_sensor_value);
|
|
||||||
|
|
||||||
cadenceGroup = view.findViewById(R.id.stats_sensor_cadence_group);
|
|
||||||
cadenceValueView = view.findViewById(R.id.stats_sensor_cadence_value);
|
|
||||||
cadenceSensorView = view.findViewById(R.id.stats_sensor_cadence_sensor_value);
|
|
||||||
|
|
||||||
totalTimeValueView = view.findViewById(R.id.stats_total_time_value);
|
|
||||||
|
|
||||||
distanceValue = view.findViewById(R.id.stats_distance_value);
|
|
||||||
distanceUnit = view.findViewById(R.id.stats_distance_unit);
|
|
||||||
|
|
||||||
activityLabel = view.findViewById(R.id.stats_activity_type_label);
|
|
||||||
activitySpinner = view.findViewById(R.id.stats_activity_type_icon);
|
|
||||||
|
|
||||||
movingTimeValue = view.findViewById(R.id.stats_moving_time_value);
|
|
||||||
|
|
||||||
speedAvgLabel = view.findViewById(R.id.stats_average_speed_label);
|
|
||||||
speedAvgValue = view.findViewById(R.id.stats_average_speed_value);
|
|
||||||
speedAvgUnit = view.findViewById(R.id.stats_average_speed_unit);
|
|
||||||
|
|
||||||
speedMaxLabel = view.findViewById(R.id.stats_max_speed_label);
|
|
||||||
speedMaxValue = view.findViewById(R.id.stats_max_speed_value);
|
|
||||||
speedMaxUnit = view.findViewById(R.id.stats_max_speed_unit);
|
|
||||||
|
|
||||||
speedMovingLabel = view.findViewById(R.id.stats_moving_speed_label);
|
|
||||||
speedMovingValue = view.findViewById(R.id.stats_moving_speed_value);
|
|
||||||
speedMovingUnit = view.findViewById(R.id.stats_moving_speed_unit);
|
|
||||||
|
|
||||||
speedLabel = view.findViewById(R.id.stats_speed_label);
|
|
||||||
speedValue = view.findViewById(R.id.stats_speed_value);
|
|
||||||
speedUnit = view.findViewById(R.id.stats_speed_unit);
|
|
||||||
|
|
||||||
elevationGroup = view.findViewById(R.id.stats_elevation_group);
|
|
||||||
elevationCurrentValue = view.findViewById(R.id.stats_elevation_current_value);
|
|
||||||
elevationCurrentUnit = view.findViewById(R.id.stats_elevation_current_unit);
|
|
||||||
|
|
||||||
elevationTotalGainValue = view.findViewById(R.id.stats_elevation_gain_value);
|
|
||||||
elevationTotalGainUnit = view.findViewById(R.id.stats_elevation_gain_unit);
|
|
||||||
|
|
||||||
coordinateGroup = view.findViewById(R.id.stats_coordinate_group);
|
|
||||||
|
|
||||||
latitudeValue = view.findViewById(R.id.stats_latitude_value);
|
|
||||||
longitudeValue = view.findViewById(R.id.stats_longitude_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onActivityCreated(Bundle savedInstanceState) {
|
public void onActivityCreated(Bundle savedInstanceState) {
|
||||||
super.onActivityCreated(savedInstanceState);
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
|
||||||
recordingGpsAccuracy = Integer.parseInt(getResources().getString(R.string.recording_gps_accuracy_default));
|
|
||||||
|
|
||||||
handlerUpdateUI = new Handler();
|
handlerUpdateUI = new Handler();
|
||||||
|
|
||||||
Spinner activityTypeIcon = getView().findViewById(R.id.stats_activity_type_icon);
|
viewBinding.statsActivityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), ""));
|
||||||
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), ""));
|
viewBinding.statsActivityTypeIcon.setOnTouchListener((v, event) -> {
|
||||||
activityTypeIcon.setOnTouchListener((v, event) -> {
|
|
||||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||||
((TrackRecordingActivity) getActivity()).chooseActivityType(category);
|
((TrackRecordingActivity) getActivity()).chooseActivityType(category);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
activityTypeIcon.setOnKeyListener((v, keyCode, event) -> {
|
viewBinding.statsActivityTypeIcon.setOnKeyListener((v, keyCode, event) -> {
|
||||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
|
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
|
||||||
((TrackRecordingActivity) getActivity()).chooseActivityType(category);
|
((TrackRecordingActivity) getActivity()).chooseActivityType(category);
|
||||||
}
|
}
|
||||||
@@ -249,52 +144,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
sensorHorizontalLine = null;
|
viewBinding = null;
|
||||||
|
|
||||||
heartRateGroup = null;
|
|
||||||
heartRateValueView = null;
|
|
||||||
heartRateSensorView = null;
|
|
||||||
cadenceGroup = null;
|
|
||||||
cadenceValueView = null;
|
|
||||||
cadenceSensorView = null;
|
|
||||||
|
|
||||||
totalTimeValueView = null;
|
|
||||||
|
|
||||||
distanceValue = null;
|
|
||||||
distanceUnit = null;
|
|
||||||
|
|
||||||
activityLabel = null;
|
|
||||||
activitySpinner = null;
|
|
||||||
|
|
||||||
movingTimeValue = null;
|
|
||||||
|
|
||||||
speedAvgLabel = null;
|
|
||||||
speedAvgValue = null;
|
|
||||||
speedAvgUnit = null;
|
|
||||||
|
|
||||||
speedMaxLabel = null;
|
|
||||||
speedMaxValue = null;
|
|
||||||
speedMaxUnit = null;
|
|
||||||
|
|
||||||
speedMovingLabel = null;
|
|
||||||
speedMovingValue = null;
|
|
||||||
speedMovingUnit = null;
|
|
||||||
|
|
||||||
speedLabel = null;
|
|
||||||
speedValue = null;
|
|
||||||
speedUnit = null;
|
|
||||||
|
|
||||||
elevationGroup = null;
|
|
||||||
elevationCurrentValue = null;
|
|
||||||
elevationCurrentUnit = null;
|
|
||||||
|
|
||||||
elevationTotalGainValue = null;
|
|
||||||
elevationTotalGainUnit = null;
|
|
||||||
|
|
||||||
coordinateGroup = null;
|
|
||||||
|
|
||||||
latitudeValue = null;
|
|
||||||
longitudeValue = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -343,9 +193,8 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
TrackPoint trackPoint = lastTrackPoint; //NOTE: There seems to be a race condition; just fix the symptom for now.
|
TrackPoint trackPoint = lastTrackPoint; //NOTE: There seems to be a race condition; just fix the symptom for now.
|
||||||
if (trackPoint != null) {
|
if (trackPoint != null) {
|
||||||
boolean hasFix = !LocationUtils.isLocationOld(trackPoint.getLocation());
|
boolean hasFix = !LocationUtils.isLocationOld(trackPoint.getLocation());
|
||||||
boolean hasGoodFix = TrackPointUtils.fulfillsAccuracy(trackPoint, recordingGpsAccuracy);
|
|
||||||
|
|
||||||
if (!hasFix || !hasGoodFix) {
|
if (!hasFix) {
|
||||||
lastTrackPoint = null;
|
lastTrackPoint = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,66 +266,58 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
private void updateSensorDataUI() {
|
private void updateSensorDataUI() {
|
||||||
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||||
|
|
||||||
SensorDataSet sensorDataSet = null;
|
|
||||||
Float elevationGain_m = null;
|
|
||||||
if (trackRecordingService == null) {
|
if (trackRecordingService == null) {
|
||||||
Log.d(TAG, "Cannot get the track recording service.");
|
Log.d(TAG, "Cannot get the track recording service.");
|
||||||
} else {
|
} else {
|
||||||
sensorDataSet = trackRecordingService.getSensorData();
|
SensorDataSet sensorDataSet = trackRecordingService.getSensorData();
|
||||||
elevationGain_m = trackRecordingService.getElevationGain_m();
|
if (sensorDataSet != null) {
|
||||||
|
setHeartRateSensorData(sensorDataSet);
|
||||||
|
setCadenceSensorData(sensorDataSet);
|
||||||
|
setSpeedSensorData(sensorDataSet, isSelectedTrackRecording());
|
||||||
|
}
|
||||||
|
|
||||||
|
setTotalElevationGain(trackRecordingService.getElevationGain_m());
|
||||||
}
|
}
|
||||||
|
|
||||||
setHeartRateSensorData(sensorDataSet);
|
|
||||||
setCadenceSensorData(sensorDataSet);
|
|
||||||
setSpeedSensorData(sensorDataSet, isSelectedTrackRecording());
|
|
||||||
|
|
||||||
setTotalElevationGain(elevationGain_m);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setHeartRateSensorData(SensorDataSet sensorDataSet) {
|
private void setHeartRateSensorData(SensorDataSet sensorDataSet) {
|
||||||
int isVisible = View.VISIBLE;
|
int isVisible = sensorDataSet.getHeartRate() != null ? View.VISIBLE : View.GONE;
|
||||||
if (PreferencesUtils.isBluetoothHeartRateSensorAddressNone(getContext())) {
|
viewBinding.statsSensorHeartRateGroup.setVisibility(isVisible);
|
||||||
isVisible = View.GONE;
|
|
||||||
}
|
|
||||||
heartRateGroup.setVisibility(isVisible);
|
|
||||||
setVisibilitySensorHorizontalLine();
|
setVisibilitySensorHorizontalLine();
|
||||||
|
|
||||||
String sensorValue = getContext().getString(R.string.value_unknown);
|
String sensorValue = getContext().getString(R.string.value_unknown);
|
||||||
String sensorName = getContext().getString(R.string.value_unknown);
|
String sensorName = getContext().getString(R.string.value_unknown);
|
||||||
if (sensorDataSet != null && sensorDataSet.getHeartRate() != null) {
|
if (sensorDataSet.getHeartRate() != null) {
|
||||||
SensorDataHeartRate data = sensorDataSet.getHeartRate();
|
SensorDataHeartRate data = sensorDataSet.getHeartRate();
|
||||||
|
|
||||||
sensorName = data.getSensorName();
|
sensorName = data.getSensorNameOrAddress();
|
||||||
if (data.hasHeartRate_bpm() && data.isRecent()) {
|
if (data.hasHeartRate_bpm() && data.isRecent()) {
|
||||||
sensorValue = StringUtils.formatDecimal(data.getHeartRate_bpm(), 0);
|
sensorValue = StringUtils.formatDecimal(data.getHeartRate_bpm(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
heartRateSensorView.setText(sensorName);
|
viewBinding.statsSensorHeartRateSensorValue.setText(sensorName);
|
||||||
heartRateValueView.setText(sensorValue);
|
viewBinding.statsSensorHeartRateValue.setText(sensorValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setCadenceSensorData(SensorDataSet sensorDataSet) {
|
private void setCadenceSensorData(SensorDataSet sensorDataSet) {
|
||||||
int isVisible = View.VISIBLE;
|
int isVisible = sensorDataSet.getCyclingCadence() != null ? View.VISIBLE : View.GONE;
|
||||||
if (PreferencesUtils.isBluetoothCyclingCadenceSensorAddressNone(getContext())) {
|
viewBinding.statsSensorCadenceGroup.setVisibility(isVisible);
|
||||||
isVisible = View.GONE;
|
|
||||||
}
|
|
||||||
cadenceGroup.setVisibility(isVisible);
|
|
||||||
setVisibilitySensorHorizontalLine();
|
setVisibilitySensorHorizontalLine();
|
||||||
|
|
||||||
String sensorValue = getContext().getString(R.string.value_unknown);
|
String sensorValue = getContext().getString(R.string.value_unknown);
|
||||||
String sensorName = getContext().getString(R.string.value_unknown);
|
String sensorName = getContext().getString(R.string.value_unknown);
|
||||||
if (sensorDataSet != null && sensorDataSet.getCyclingCadence() != null) {
|
if (sensorDataSet.getCyclingCadence() != null) {
|
||||||
SensorDataCycling.Cadence data = sensorDataSet.getCyclingCadence();
|
SensorDataCycling.Cadence data = sensorDataSet.getCyclingCadence();
|
||||||
sensorName = data.getSensorName();
|
sensorName = data.getSensorNameOrAddress();
|
||||||
|
|
||||||
if (data.hasCadence_rpm() && data.isRecent()) {
|
if (data.hasCadence_rpm() && data.isRecent()) {
|
||||||
sensorValue = StringUtils.formatDecimal(data.getCadence_rpm(), 0);
|
sensorValue = StringUtils.formatDecimal(data.getCadence_rpm(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cadenceSensorView.setText(sensorName);
|
viewBinding.statsSensorCadenceSensorValue.setText(sensorName);
|
||||||
cadenceValueView.setText(sensorValue);
|
viewBinding.statsSensorCadenceValue.setText(sensorValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set elevation gain
|
// Set elevation gain
|
||||||
@@ -495,16 +336,16 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationGain, metricUnits);
|
Pair<String, String> parts = StringUtils.formatElevation(getContext(), totalElevationGain, metricUnits);
|
||||||
elevationTotalGainValue.setText(parts.first);
|
viewBinding.statsElevationGainValue.setText(parts.first);
|
||||||
elevationTotalGainUnit.setText(parts.second);
|
viewBinding.statsElevationGainUnit.setText(parts.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If cadence and hear rate groups are invisible then sensor horizontal line hast to be invisible too.
|
* If cadence and hear rate groups are invisible then sensor horizontal line hast to be invisible too.
|
||||||
*/
|
*/
|
||||||
private void setVisibilitySensorHorizontalLine() {
|
private void setVisibilitySensorHorizontalLine() {
|
||||||
if (cadenceGroup.getVisibility() != View.VISIBLE && heartRateGroup.getVisibility() != View.VISIBLE) {
|
if (viewBinding.statsSensorCadenceGroup.getVisibility() != View.VISIBLE && viewBinding.statsSensorHeartRateGroup.getVisibility() != View.VISIBLE) {
|
||||||
sensorHorizontalLine.setVisibility(View.GONE);
|
viewBinding.statsSensorHorizontalLine.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,64 +373,59 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
double totalDistance = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getTotalDistance();
|
double totalDistance = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getTotalDistance();
|
||||||
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
|
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, metricUnits);
|
||||||
|
|
||||||
distanceValue.setText(parts.first);
|
viewBinding.statsDistanceValue.setText(parts.first);
|
||||||
distanceUnit.setText(parts.second);
|
viewBinding.statsDistanceUnit.setText(parts.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set activity type
|
// Set activity type
|
||||||
{
|
{
|
||||||
activityLabel.setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE);
|
viewBinding.statsActivityTypeIcon.setEnabled(isRecording);
|
||||||
|
TrackIconUtils.setIconSpinner(viewBinding.statsActivityTypeIcon, trackIconValue);
|
||||||
activitySpinner.setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE);
|
|
||||||
activitySpinner.setEnabled(isRecording);
|
|
||||||
if (trackIconValue != null) {
|
|
||||||
TrackIconUtils.setIconSpinner(activitySpinner, trackIconValue);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set time
|
// Set time
|
||||||
if (lastTrackStatistics != null) {
|
if (lastTrackStatistics != null) {
|
||||||
movingTimeValue.setText(StringUtils.formatElapsedTime(lastTrackStatistics.getMovingTime()));
|
viewBinding.statsMovingTimeValue.setText(StringUtils.formatElapsedTime(lastTrackStatistics.getMovingTime()));
|
||||||
updateTotalTime();
|
updateTotalTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set average speed/pace
|
// Set average speed/pace
|
||||||
{
|
{
|
||||||
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageSpeed() : Double.NaN;
|
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageSpeed() : Double.NaN;
|
||||||
speedAvgLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
|
viewBinding.statsAverageSpeedLabel.setText(reportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
|
||||||
|
|
||||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
||||||
speedAvgValue.setText(parts.first);
|
viewBinding.statsAverageSpeedValue.setText(parts.first);
|
||||||
speedAvgUnit.setText(parts.second);
|
viewBinding.statsAverageSpeedUnit.setText(parts.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set max speed/pace
|
// Set max speed/pace
|
||||||
{
|
{
|
||||||
double speed = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getMaxSpeed();
|
double speed = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getMaxSpeed();
|
||||||
|
|
||||||
speedMaxLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
|
viewBinding.statsMaxSpeedLabel.setText(reportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
|
||||||
|
|
||||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
||||||
speedMaxValue.setText(parts.first);
|
viewBinding.statsMaxSpeedValue.setText(parts.first);
|
||||||
speedMaxUnit.setText(parts.second);
|
viewBinding.statsMaxSpeedUnit.setText(parts.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set moving speed/pace
|
// Set moving speed/pace
|
||||||
{
|
{
|
||||||
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageMovingSpeed() : Double.NaN;
|
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageMovingSpeed() : Double.NaN;
|
||||||
|
|
||||||
speedMovingLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
|
viewBinding.statsMovingSpeedLabel.setText(reportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
|
||||||
|
|
||||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
||||||
speedMovingValue.setText(parts.first);
|
viewBinding.statsMovingSpeedValue.setText(parts.first);
|
||||||
speedMovingUnit.setText(parts.second);
|
viewBinding.statsMovingSpeedUnit.setText(parts.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set elevation (gain)
|
// Set elevation (gain)
|
||||||
{
|
{
|
||||||
// Make elevation visible?
|
// Make elevation visible?
|
||||||
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
|
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
|
||||||
elevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
|
viewBinding.statsElevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,7 +436,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
} else {
|
} else {
|
||||||
totalTime = lastTrackStatistics.getTotalTime();
|
totalTime = lastTrackStatistics.getTotalTime();
|
||||||
}
|
}
|
||||||
totalTimeValueView.setText(StringUtils.formatElapsedTime(totalTime));
|
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(totalTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -625,28 +461,28 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
|
|
||||||
// Set elevation
|
// Set elevation
|
||||||
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
|
boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext());
|
||||||
elevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
|
viewBinding.statsElevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
if (showElevation) {
|
if (showElevation) {
|
||||||
// Current elevation
|
// Current elevation
|
||||||
Float altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? (float) lastTrackPoint.getAltitude() : null;
|
Float altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? (float) lastTrackPoint.getAltitude() : null;
|
||||||
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, metricUnits);
|
Pair<String, String> parts = StringUtils.formatElevation(getContext(), altitude, metricUnits);
|
||||||
elevationCurrentValue.setText(parts.first);
|
viewBinding.statsElevationCurrentValue.setText(parts.first);
|
||||||
elevationCurrentUnit.setText(parts.second);
|
viewBinding.statsElevationCurrentUnit.setText(parts.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set coordinate
|
// Set coordinate
|
||||||
boolean showCoordinate = PreferencesUtils.isStatsShowCoordinate(getContext());
|
boolean showCoordinate = PreferencesUtils.isStatsShowCoordinate(getContext());
|
||||||
|
|
||||||
coordinateGroup.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
|
viewBinding.statsCoordinateGroup.setVisibility(showCoordinate ? View.VISIBLE : View.GONE);
|
||||||
if (showCoordinate) {
|
if (showCoordinate) {
|
||||||
double latitude = lastTrackPoint != null ? lastTrackPoint.getLatitude() : Double.NaN;
|
double latitude = lastTrackPoint != null ? lastTrackPoint.getLatitude() : Double.NaN;
|
||||||
String latitudeText = Double.isNaN(latitude) || Double.isInfinite(latitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(latitude);
|
String latitudeText = Double.isNaN(latitude) || Double.isInfinite(latitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(latitude);
|
||||||
latitudeValue.setText(latitudeText);
|
viewBinding.statsLatitudeValue.setText(latitudeText);
|
||||||
|
|
||||||
double longitude = lastTrackPoint != null ? lastTrackPoint.getLongitude() : Double.NaN;
|
double longitude = lastTrackPoint != null ? lastTrackPoint.getLongitude() : Double.NaN;
|
||||||
String longitudeText = Double.isNaN(longitude) || Double.isInfinite(longitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(longitude);
|
String longitudeText = Double.isNaN(longitude) || Double.isInfinite(longitude) ? getContext().getString(R.string.value_unknown) : StringUtils.formatCoordinate(longitude);
|
||||||
longitudeValue.setText(longitudeText);
|
viewBinding.statsLongitudeValue.setText(longitudeText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,10 +490,10 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
|
|||||||
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
|
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
|
||||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
|
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
|
||||||
|
|
||||||
speedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
|
viewBinding.statsSpeedLabel.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
|
||||||
|
|
||||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, metricUnits, reportSpeed);
|
||||||
speedValue.setText(parts.first);
|
viewBinding.statsSpeedValue.setText(parts.first);
|
||||||
speedUnit.setText(parts.second);
|
viewBinding.statsSpeedUnit.setText(parts.second);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package de.dennisguse.opentracks.io.file;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.R;
|
||||||
|
|
||||||
|
public class ErrorListDialog extends DialogFragment {
|
||||||
|
|
||||||
|
public static final String TAG = ErrorListDialog.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final String EXTRA_TITLE = "extra_title";
|
||||||
|
private static final String EXTRA_ERROR_LIST = "extra_error_list";
|
||||||
|
|
||||||
|
private ArrayList<String> errorList;
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
public static void showDialog(FragmentManager fragmentManager, String title, ArrayList<String> errorList) {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString(EXTRA_TITLE, title);
|
||||||
|
bundle.putStringArrayList(EXTRA_ERROR_LIST, errorList);
|
||||||
|
|
||||||
|
ErrorListDialog errorListDialog = new ErrorListDialog();
|
||||||
|
errorListDialog.setArguments(bundle);
|
||||||
|
errorListDialog.setRetainInstance(true);
|
||||||
|
errorListDialog.show(fragmentManager, TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
title = getArguments().getString(EXTRA_TITLE);
|
||||||
|
errorList = getArguments().getStringArrayList(EXTRA_ERROR_LIST);
|
||||||
|
} else {
|
||||||
|
title = savedInstanceState.getString(EXTRA_TITLE);
|
||||||
|
errorList = savedInstanceState.getStringArrayList(EXTRA_ERROR_LIST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
outState.putString(EXTRA_TITLE, title);
|
||||||
|
outState.putStringArrayList(EXTRA_ERROR_LIST, errorList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
|
final String[] tracks = errorList.stream().toArray(String[]::new);
|
||||||
|
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getActivity())
|
||||||
|
.setTitle(title)
|
||||||
|
.setItems(tracks, null)
|
||||||
|
.setPositiveButton(R.string.generic_ok, (dialog, which) -> dismiss());
|
||||||
|
AlertDialog dialog = alertDialogBuilder.create();
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package de.dennisguse.opentracks.io.file;
|
|
||||||
|
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
|
||||||
|
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
|
||||||
|
|
||||||
public class ExportFile {
|
|
||||||
|
|
||||||
public static DocumentFile getExportDocumentFile(Track.Id trackId, String trackFileFormatExtension, DocumentFile directory, String mimeType) {
|
|
||||||
String fileName = getExportFileNameByTrackId(trackId, trackFileFormatExtension);
|
|
||||||
DocumentFile file = directory.findFile(fileName);
|
|
||||||
if (file == null) {
|
|
||||||
file = directory.createFile(mimeType, fileName);
|
|
||||||
}
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getExportFileNameByTrackId(Track.Id trackId, String trackFileFormatExtension) {
|
|
||||||
return trackId.getId() + "." + trackFileFormatExtension;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,58 +16,381 @@
|
|||||||
|
|
||||||
package de.dennisguse.opentracks.io.file.exporter;
|
package de.dennisguse.opentracks.io.file.exporter;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
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.fragment.app.FragmentActivity;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.fragments.ExportFinishedDialogFragment;
|
import java.util.ArrayList;
|
||||||
import de.dennisguse.opentracks.fragments.ExportProgressDialogFragment;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.R;
|
||||||
|
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.databinding.ExportActivityBinding;
|
||||||
|
import de.dennisguse.opentracks.io.file.ErrorListDialog;
|
||||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||||
|
import de.dennisguse.opentracks.util.ExportUtils;
|
||||||
|
import de.dennisguse.opentracks.util.FileUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An activity for saving tracks to the external storage.
|
* An activity for saving tracks to the external storage.
|
||||||
*
|
*
|
||||||
* @author Rodrigo Damazio
|
* @author Rodrigo Damazio
|
||||||
*/
|
*/
|
||||||
public class ExportActivity extends FragmentActivity implements ExportProgressDialogFragment.DismissCallback, ExportFinishedDialogFragment.DismissCallback {
|
public class ExportActivity extends FragmentActivity implements ExportServiceResultReceiver.Receiver {
|
||||||
|
|
||||||
|
private static final String TAG = ExportActivity.class.getSimpleName();
|
||||||
|
|
||||||
public static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
|
public static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
|
||||||
|
|
||||||
public static final String EXTRA_TRACKFILEFORMAT_KEY = "trackfileformat";
|
public static final String EXTRA_TRACKFILEFORMAT_KEY = "trackfileformat";
|
||||||
|
|
||||||
private static final String BUNDLE_ACTIVITY_RECREATED = "activity_recreated";
|
private static final String BUNDLE_AUTO_CONFLICT = "auto_conflict";
|
||||||
|
private static final String BUNDLE_SUCCESS_COUNT = "track_export_success_count";
|
||||||
|
private static final String BUNDLE_ERROR_COUNT = "track_export_error_ount";
|
||||||
|
private static final String BUNDLE_OVERWRITTEN_COUNT = "track_export_overwritten_count";
|
||||||
|
private static final String BUNDLE_SKIPPED_COUNT = "track_export_skipped_count";
|
||||||
|
private static final String BUNDLE_TOTAL_COUNT = "track_export_total_count";
|
||||||
|
private static final String BUNDLE_DIRECTORY_FILES = "track_directory_files";
|
||||||
|
private static final String BUNDLE_TRACK_ERRORS = "track_errors";
|
||||||
|
|
||||||
|
private static final int CONFLICT_NONE = 0;
|
||||||
|
private static final int CONFLICT_OVERWRITE = 1;
|
||||||
|
private static final int CONFLICT_SKIP = 2;
|
||||||
|
|
||||||
|
private TrackFileFormat trackFileFormat;
|
||||||
|
private Uri directoryUri;
|
||||||
|
|
||||||
|
private ExportServiceResultReceiver resultReceiver;
|
||||||
|
|
||||||
|
private String directoryDisplayName;
|
||||||
|
private List<String> directoryFiles;
|
||||||
|
|
||||||
|
private int trackExportSuccessCount;
|
||||||
|
private int trackExportErrorCount;
|
||||||
|
private int trackExportOverwrittenCount;
|
||||||
|
private int trackExportSkippedCount;
|
||||||
|
private int trackExportTotalCount;
|
||||||
|
|
||||||
|
boolean doubleBackToCancel = false;
|
||||||
|
|
||||||
|
private ExportActivityBinding viewBinding;
|
||||||
|
|
||||||
|
private ArrayList<String> trackErrors = new ArrayList<>();
|
||||||
|
|
||||||
|
private int autoConflict;
|
||||||
|
|
||||||
|
private ContentProviderUtils contentProviderUtils;
|
||||||
|
|
||||||
|
// List of tracks to be exported.
|
||||||
|
private final ArrayList<Track.Id> trackIds = new ArrayList<>();
|
||||||
|
|
||||||
|
private final LinkedBlockingQueue<PendingConflict> conflictsQueue = new LinkedBlockingQueue<>();
|
||||||
|
private final Handler conflictsHandler = new Handler();
|
||||||
|
|
||||||
|
private final Runnable conflictsRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (conflictsQueue.size() > 0) {
|
||||||
|
PendingConflict conflict = conflictsQueue.peek();
|
||||||
|
if (conflict.resolve()) {
|
||||||
|
conflictsQueue.remove(conflict);
|
||||||
|
if (!conflictsQueue.isEmpty()) {
|
||||||
|
conflictsHandler.post(conflictsRunnable);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
viewBinding.exportProgressLeftButton.setOnClickListener((view) -> {
|
||||||
|
setConflictVisibility(View.GONE);
|
||||||
|
conflict.skip();
|
||||||
|
conflictsQueue.remove(conflict);
|
||||||
|
if (!conflictsQueue.isEmpty()) {
|
||||||
|
conflictsHandler.post(conflictsRunnable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
viewBinding.exportProgressRightButton.setOnClickListener((view) -> {
|
||||||
|
setConflictVisibility(View.GONE);
|
||||||
|
conflict.overwrite();
|
||||||
|
conflictsQueue.remove(conflict);
|
||||||
|
if (!conflictsQueue.isEmpty()) {
|
||||||
|
conflictsHandler.post(conflictsRunnable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
viewBinding = ExportActivityBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(viewBinding.getRoot());
|
||||||
|
|
||||||
if (savedInstanceState == null || !savedInstanceState.getBoolean(BUNDLE_ACTIVITY_RECREATED, false)) {
|
directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
|
||||||
Uri directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
|
trackFileFormat = (TrackFileFormat) getIntent().getSerializableExtra(EXTRA_TRACKFILEFORMAT_KEY);
|
||||||
TrackFileFormat trackfileformat = (TrackFileFormat) getIntent().getSerializableExtra(EXTRA_TRACKFILEFORMAT_KEY);
|
|
||||||
ExportProgressDialogFragment.showDialog(getSupportFragmentManager(), trackfileformat, directoryUri);
|
contentProviderUtils = new ContentProviderUtils(this);
|
||||||
|
|
||||||
|
DocumentFile documentFile = DocumentFile.fromTreeUri(this, directoryUri);
|
||||||
|
directoryDisplayName = FileUtils.getPath(documentFile);
|
||||||
|
|
||||||
|
viewBinding.toolbar.toolbar.setTitle(getString(R.string.export_progress_message, directoryDisplayName));
|
||||||
|
|
||||||
|
resultReceiver = new ExportServiceResultReceiver(new Handler(), this);
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
autoConflict = CONFLICT_NONE;
|
||||||
|
setProgress();
|
||||||
|
new Thread(() -> {
|
||||||
|
directoryFiles = ExportUtils.getAllFiles(ExportActivity.this, documentFile.getUri());
|
||||||
|
runOnUiThread(() -> initExport(0));
|
||||||
|
}).start();
|
||||||
|
} else {
|
||||||
|
autoConflict = savedInstanceState.getInt(BUNDLE_AUTO_CONFLICT);
|
||||||
|
trackExportSuccessCount = savedInstanceState.getInt(BUNDLE_SUCCESS_COUNT);
|
||||||
|
trackExportErrorCount = savedInstanceState.getInt(BUNDLE_ERROR_COUNT);
|
||||||
|
trackExportOverwrittenCount = savedInstanceState.getInt(BUNDLE_OVERWRITTEN_COUNT);
|
||||||
|
trackExportSkippedCount = savedInstanceState.getInt(BUNDLE_SKIPPED_COUNT);
|
||||||
|
trackExportTotalCount = savedInstanceState.getInt(BUNDLE_TOTAL_COUNT);
|
||||||
|
directoryFiles = savedInstanceState.getStringArrayList(BUNDLE_DIRECTORY_FILES);
|
||||||
|
trackErrors = savedInstanceState.getStringArrayList(BUNDLE_TRACK_ERRORS);
|
||||||
|
|
||||||
|
setProgress();
|
||||||
|
initExport(getTotalDone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putBoolean(BUNDLE_ACTIVITY_RECREATED, true);
|
outState.putInt(BUNDLE_AUTO_CONFLICT, autoConflict);
|
||||||
|
outState.putInt(BUNDLE_SUCCESS_COUNT, trackExportSuccessCount);
|
||||||
|
outState.putInt(BUNDLE_ERROR_COUNT, trackExportErrorCount);
|
||||||
|
outState.putInt(BUNDLE_OVERWRITTEN_COUNT, trackExportOverwrittenCount);
|
||||||
|
outState.putInt(BUNDLE_SKIPPED_COUNT, trackExportSkippedCount);
|
||||||
|
outState.putInt(BUNDLE_TOTAL_COUNT, trackExportTotalCount);
|
||||||
|
outState.putStringArrayList(BUNDLE_DIRECTORY_FILES, (ArrayList<String>) directoryFiles);
|
||||||
|
outState.putStringArrayList(BUNDLE_TRACK_ERRORS, trackErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExportCanceled(String directoryDisplayName, int successCount, int fileCount) {
|
protected void onDestroy() {
|
||||||
onExportFinished(directoryDisplayName, successCount, fileCount);
|
super.onDestroy();
|
||||||
|
conflictsQueue.clear();
|
||||||
|
trackIds.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onExportFinished(final String directoryDisplayName, final int successCount, final int fileCount) {
|
public void onBackPressed() {
|
||||||
runOnUiThread(() -> ExportFinishedDialogFragment.showDialog(getSupportFragmentManager(), directoryDisplayName, successCount, fileCount));
|
if (doubleBackToCancel || getTotalDone() == trackExportTotalCount) {
|
||||||
|
super.onBackPressed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.doubleBackToCancel = true;
|
||||||
|
Toast.makeText(this, getString(R.string.generic_click_twice_cancel), Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
|
new Handler().postDelayed(() -> doubleBackToCancel=false, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initExport(int from) {
|
||||||
|
try (Cursor cursor = contentProviderUtils.getTrackCursor(null, null, TracksColumns._ID)) {
|
||||||
|
if (cursor == null) {
|
||||||
|
onExportEnded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
trackExportTotalCount = cursor.getCount();
|
||||||
|
viewBinding.exportProgressTotal.setText("" + trackExportTotalCount);
|
||||||
|
for (int i = from; i < trackExportTotalCount; i++) {
|
||||||
|
cursor.moveToPosition(i);
|
||||||
|
Track track = ContentProviderUtils.createTrack(cursor);
|
||||||
|
trackIds.add(track.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trackIds.isEmpty()) {
|
||||||
|
export(trackIds.get(0));
|
||||||
|
} else {
|
||||||
|
onExportEnded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue track identified by trackId to be exported if not exported already or there is a conflict resolution.
|
||||||
|
*
|
||||||
|
* @param trackId the track's id.
|
||||||
|
* @param conflictResolution conflict resolution to be applied if needed.
|
||||||
|
*/
|
||||||
|
private void export(Track.Id trackId, int conflictResolution) {
|
||||||
|
boolean fileExists = ExportUtils.isExportFileExists(trackId, trackFileFormat.getExtension(), directoryFiles);
|
||||||
|
|
||||||
|
if (fileExists && conflictResolution == CONFLICT_NONE) {
|
||||||
|
conflict(trackId);
|
||||||
|
} else if (fileExists && conflictResolution == CONFLICT_SKIP) {
|
||||||
|
trackExportSkippedCount++;
|
||||||
|
onExportCompleted(trackId);
|
||||||
|
} else {
|
||||||
|
ExportService.enqueue(this, resultReceiver, trackId, trackFileFormat, directoryUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void export(Track.Id trackId) {
|
||||||
|
export(trackId, autoConflict);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setConflictVisibility(int visibility) {
|
||||||
|
viewBinding.exportProgressAlertIcon.setVisibility(visibility);
|
||||||
|
viewBinding.exportProgressAlertMsg.setVisibility(visibility);
|
||||||
|
viewBinding.exportProgressApplyToAll.setVisibility(visibility);
|
||||||
|
viewBinding.exportProgressLeftButton.setVisibility(visibility);
|
||||||
|
viewBinding.exportProgressRightButton.setVisibility(visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getTotalDone() {
|
||||||
|
return trackExportSuccessCount + trackExportOverwrittenCount + trackExportSkippedCount + trackExportErrorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setProgress() {
|
||||||
|
int done = getTotalDone();
|
||||||
|
|
||||||
|
viewBinding.exportProgressDone.setText("" + done);
|
||||||
|
viewBinding.exportProgressTotal.setText("" + trackExportTotalCount);
|
||||||
|
|
||||||
|
viewBinding.exportProgressBar.setProgress((int) ((float) done / (float) trackExportTotalCount * 100f));
|
||||||
|
viewBinding.exportProgressSummary.setText(getString(R.string.export_progress_review, getTotalDone(), trackExportSuccessCount, trackExportOverwrittenCount, trackExportSkippedCount, trackExportErrorCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onExportCompleted(Track.Id trackId) {
|
||||||
|
trackIds.remove(trackId);
|
||||||
|
setProgress();
|
||||||
|
if (trackIds.isEmpty()) {
|
||||||
|
onExportEnded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
export(trackIds.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onExportEnded() {
|
||||||
|
viewBinding.exportProgressRightButton.setVisibility(View.VISIBLE);
|
||||||
|
viewBinding.exportProgressRightButton.setText(getString(R.string.generic_ok));
|
||||||
|
viewBinding.exportProgressRightButton.setOnClickListener((view) -> finish());
|
||||||
|
|
||||||
|
viewBinding.exportProgressAlertIcon.setVisibility(View.VISIBLE);
|
||||||
|
viewBinding.exportProgressAlertMsg.setVisibility(View.VISIBLE);
|
||||||
|
if (trackExportErrorCount > 0) {
|
||||||
|
viewBinding.exportProgressLeftButton.setVisibility(View.VISIBLE);
|
||||||
|
viewBinding.exportProgressLeftButton.setText(getString(R.string.generic_show_errors));
|
||||||
|
viewBinding.exportProgressLeftButton.setOnClickListener((view) -> ErrorListDialog.showDialog(getSupportFragmentManager(), getString(R.string.export_track_errors), trackErrors));
|
||||||
|
viewBinding.exportProgressAlertIcon.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_report_problem_24));
|
||||||
|
String msg = getResources().getQuantityString(R.plurals.generic_completed_with_errors, trackExportErrorCount, trackExportErrorCount);
|
||||||
|
viewBinding.exportProgressAlertMsg.setText(msg);
|
||||||
|
} else {
|
||||||
|
viewBinding.exportProgressLeftButton.setVisibility(View.GONE);
|
||||||
|
viewBinding.exportProgressAlertIcon.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.ic_dialog_success_24dp));
|
||||||
|
viewBinding.exportProgressAlertMsg.setText(getString(R.string.generic_completed));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDismissed() {
|
public void onReceiveResult(int resultCode, Bundle resultData) {
|
||||||
finish();
|
if (resultData == null) {
|
||||||
|
throw new RuntimeException(TAG + ": onReceiveResult resultData NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
Track.Id trackId = resultData.getParcelable(ExportServiceResultReceiver.RESULT_EXTRA_TRACK_ID);
|
||||||
|
|
||||||
|
switch (resultCode) {
|
||||||
|
case ExportServiceResultReceiver.RESULT_CODE_ERROR:
|
||||||
|
trackExportErrorCount++;
|
||||||
|
Track track = contentProviderUtils.getTrack(trackId);
|
||||||
|
trackErrors.add(track.getName());
|
||||||
|
break;
|
||||||
|
case ExportServiceResultReceiver.RESULT_CODE_SUCCESS:
|
||||||
|
if (ExportUtils.isExportFileExists(trackId, trackFileFormat.getExtension(), directoryFiles)) {
|
||||||
|
trackExportOverwrittenCount++;
|
||||||
|
} else {
|
||||||
|
trackExportSuccessCount++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new RuntimeException(TAG + ": export service result code invalid: " + resultCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
onExportCompleted(trackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void conflict(Track.Id trackId) {
|
||||||
|
PendingConflict newConflict = new PendingConflict(trackId);
|
||||||
|
conflictsQueue.add(newConflict);
|
||||||
|
|
||||||
|
if (conflictsQueue.size() == 1) {
|
||||||
|
conflictsHandler.post(conflictsRunnable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle conflicts (exporting file already exists).
|
||||||
|
*/
|
||||||
|
private class PendingConflict {
|
||||||
|
private final Track.Id trackId;
|
||||||
|
|
||||||
|
public PendingConflict(Track.Id trackId) {
|
||||||
|
this.trackId = trackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to resolve the conflict if user gave the info needed.
|
||||||
|
* Otherwise shows the buttons and views needed to gives the user the possibility of take a decision.
|
||||||
|
*
|
||||||
|
* @return true if it could resolve the conflict or false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean resolve() {
|
||||||
|
if (autoConflict == CONFLICT_NONE) {
|
||||||
|
Track track = contentProviderUtils.getTrack(trackId);
|
||||||
|
viewBinding.exportProgressAlertIcon.setImageDrawable(getDrawable(R.drawable.ic_report_problem_24));
|
||||||
|
viewBinding.exportProgressAlertMsg.setText(getString(R.string.export_track_already_exists_msg, track.getName()));
|
||||||
|
setConflictVisibility(View.VISIBLE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export(trackId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overwrite the export file and set the autoConflict if user set the "do it for all" switch button.
|
||||||
|
*/
|
||||||
|
public void overwrite() {
|
||||||
|
export(trackId, CONFLICT_OVERWRITE);
|
||||||
|
|
||||||
|
if (viewBinding.exportProgressApplyToAll.isChecked()) {
|
||||||
|
autoConflict = CONFLICT_OVERWRITE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skip the export file and set the autoConflict if user set the "do it for all" switch button.
|
||||||
|
*/
|
||||||
|
public void skip() {
|
||||||
|
export(trackId, CONFLICT_SKIP);
|
||||||
|
|
||||||
|
if (viewBinding.exportProgressApplyToAll.isChecked()) {
|
||||||
|
autoConflict = CONFLICT_SKIP;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package de.dennisguse.opentracks.io.file.exporter;
|
||||||
|
|
||||||
|
import android.app.job.JobService;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.ResultReceiver;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.app.JobIntentService;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
|
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||||
|
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||||
|
import de.dennisguse.opentracks.util.ExportUtils;
|
||||||
|
|
||||||
|
public class ExportService extends JobIntentService {
|
||||||
|
|
||||||
|
private static final int JOB_ID = 1;
|
||||||
|
|
||||||
|
private static final String EXTRA_RECEIVER = "extra_receiver";
|
||||||
|
private static final String EXTRA_TRACK_ID = "extra_track_id";
|
||||||
|
private static final String EXTRA_TRACK_FILE_FORMAT = "extra_track_file_format";
|
||||||
|
private static final String EXTRA_DIRECTORY_URI = "extra_directory_uri";
|
||||||
|
|
||||||
|
public static void enqueue(Context context, ExportServiceResultReceiver receiver, Track.Id trackId, TrackFileFormat trackFileFormat, Uri directoryUri) {
|
||||||
|
Intent intent = new Intent(context, JobService.class);
|
||||||
|
intent.putExtra(EXTRA_RECEIVER, receiver);
|
||||||
|
intent.putExtra(EXTRA_TRACK_ID, trackId);
|
||||||
|
intent.putExtra(EXTRA_TRACK_FILE_FORMAT, trackFileFormat);
|
||||||
|
intent.putExtra(EXTRA_DIRECTORY_URI, directoryUri);
|
||||||
|
enqueueWork(context, ExportService.class, JOB_ID, intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHandleWork(@NonNull Intent intent) {
|
||||||
|
// Get all data.
|
||||||
|
ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER);
|
||||||
|
Track.Id trackId = intent.getParcelableExtra(EXTRA_TRACK_ID);
|
||||||
|
TrackFileFormat trackFileFormat = (TrackFileFormat) intent.getSerializableExtra(EXTRA_TRACK_FILE_FORMAT);
|
||||||
|
Uri directoryUri = intent.getParcelableExtra(EXTRA_DIRECTORY_URI);
|
||||||
|
|
||||||
|
// Build directory file.
|
||||||
|
DocumentFile directoryFile = DocumentFile.fromTreeUri(this, directoryUri);
|
||||||
|
|
||||||
|
// Export.
|
||||||
|
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
|
||||||
|
Track track = contentProviderUtils.getTrack(trackId);
|
||||||
|
boolean success = ExportUtils.exportTrack(this, trackFileFormat, directoryFile, track);
|
||||||
|
|
||||||
|
// Prepare resultCode and bundle to send to the receiver.
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putParcelable(ExportServiceResultReceiver.RESULT_EXTRA_TRACK_ID, trackId);
|
||||||
|
|
||||||
|
// Send result to the receiver.
|
||||||
|
int resultCode = success ? ExportServiceResultReceiver.RESULT_CODE_SUCCESS : ExportServiceResultReceiver.RESULT_CODE_ERROR;
|
||||||
|
resultReceiver.send(resultCode, bundle);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package de.dennisguse.opentracks.io.file.exporter;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.ResultReceiver;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ResultReceive to receive results.
|
||||||
|
* Your {@link #onReceiveResult} method will be called from the thread running <var>handler</var> if given, or from an arbitrary thread if null.
|
||||||
|
*/
|
||||||
|
public class ExportServiceResultReceiver extends ResultReceiver {
|
||||||
|
|
||||||
|
public static final int RESULT_CODE_SUCCESS = 1;
|
||||||
|
public static final int RESULT_CODE_ERROR = 0;
|
||||||
|
|
||||||
|
public static final String RESULT_EXTRA_TRACK_ID = "result_extra_track_id";
|
||||||
|
|
||||||
|
private final Receiver receiver;
|
||||||
|
|
||||||
|
public ExportServiceResultReceiver(Handler handler, @NonNull Receiver receiver) {
|
||||||
|
super(handler);
|
||||||
|
this.receiver = receiver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||||
|
receiver.onReceiveResult(resultCode, resultData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Receiver {
|
||||||
|
void onReceiveResult(int resultCode, Bundle resultData);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -134,13 +134,18 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
|||||||
saxParser.parse(inputStream, this);
|
saxParser.parse(inputStream, this);
|
||||||
Log.d(TAG, "Total import time: " + (System.currentTimeMillis() - start) + "ms");
|
Log.d(TAG, "Total import time: " + (System.currentTimeMillis() - start) + "ms");
|
||||||
if (trackIds.size() != 1) {
|
if (trackIds.size() != 1) {
|
||||||
throw new MultiTracksImportException();
|
// TODO Multi track is not supported yet.
|
||||||
|
throw new ImportParserException("Multi track not supported");
|
||||||
}
|
}
|
||||||
return trackIds.get(0);
|
return trackIds.get(0);
|
||||||
} catch (MultiTracksImportException | IOException | SAXException | SQLiteConstraintException | ParserConfigurationException e) {
|
} catch (IOException | SAXException | ParserConfigurationException e) {
|
||||||
Log.e(TAG, "Unable to import file", e);
|
Log.e(TAG, "Unable to import file", e);
|
||||||
cleanImport();
|
cleanImport();
|
||||||
return null;
|
throw new ImportParserException(e);
|
||||||
|
} catch (SQLiteConstraintException e) {
|
||||||
|
Log.e(TAG, "Unable to import file", e);
|
||||||
|
cleanImport();
|
||||||
|
throw new ImportAlreadyExistsException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,9 +519,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
|||||||
* Cleans up import.
|
* Cleans up import.
|
||||||
*/
|
*/
|
||||||
private void cleanImport() {
|
private void cleanImport() {
|
||||||
for (Track.Id trackId : trackIds) {
|
contentProviderUtils.deleteTracks(context, trackIds);
|
||||||
contentProviderUtils.deleteTrack(context, trackId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -550,9 +553,4 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
|
|||||||
// The number of buffered locations
|
// The number of buffered locations
|
||||||
int numBufferedTrackPoints = 0;
|
int numBufferedTrackPoints = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
//TODO According to the reader AND writer, we support multiple tracks per file; we should implement it here also.
|
|
||||||
private static class MultiTracksImportException extends RuntimeException {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import android.app.Activity;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.io.file.exporter.ExportActivity;
|
import de.dennisguse.opentracks.io.file.exporter.ExportActivity;
|
||||||
import de.dennisguse.opentracks.util.IntentUtils;
|
import de.dennisguse.opentracks.util.IntentUtils;
|
||||||
@@ -21,31 +23,46 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
configureDirectoryChooserIntent(intent);
|
DocumentFile directoryUri = configureDirectoryChooserIntent(intent);
|
||||||
|
if (directoryUri == null) {
|
||||||
startActivityForResult(intent, DIRECTORY_PICKER_REQUEST_CODE);
|
startActivityForResult(intent, DIRECTORY_PICKER_REQUEST_CODE);
|
||||||
|
} else {
|
||||||
|
startActivity(createNextActivityIntent(directoryUri.getUri())); //TODO Refactor to DocumentFile
|
||||||
|
finish();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent resultData) {
|
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent resultData) {
|
||||||
super.onActivityResult(requestCode, resultCode, resultData);
|
super.onActivityResult(requestCode, resultCode, resultData);
|
||||||
|
onActivityResultCustom(requestCode, resultCode, resultData);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onActivityResultCustom(int requestCode, int resultCode, @Nullable Intent resultData) {
|
||||||
if (requestCode == DIRECTORY_PICKER_REQUEST_CODE) {
|
if (requestCode == DIRECTORY_PICKER_REQUEST_CODE) {
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
Uri directoryUri = resultData.getData();
|
Uri directoryUri = resultData.getData();
|
||||||
|
|
||||||
|
int takeFlags = resultData.getFlags();
|
||||||
|
takeFlags &= (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||||
|
getContentResolver().takePersistableUriPermission(directoryUri, takeFlags);
|
||||||
|
|
||||||
startActivity(createNextActivityIntent(directoryUri));
|
startActivity(createNextActivityIntent(directoryUri));
|
||||||
}
|
}
|
||||||
finish();
|
|
||||||
}
|
}
|
||||||
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void configureDirectoryChooserIntent(Intent intent) {
|
/**
|
||||||
|
* @return null if directory needs to be selected.
|
||||||
|
*/
|
||||||
|
protected DocumentFile configureDirectoryChooserIntent(Intent intent) {
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract Intent createNextActivityIntent(Uri directoryUri);
|
protected abstract Intent createNextActivityIntent(Uri directoryUri);
|
||||||
|
|
||||||
|
|
||||||
public static class ImportDirectoryChooserActivity extends DirectoryChooserActivity {
|
public static class ImportDirectoryChooserActivity extends DirectoryChooserActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -58,6 +75,14 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
public static class ExportDirectoryChooserActivity extends DirectoryChooserActivity {
|
public static class ExportDirectoryChooserActivity extends DirectoryChooserActivity {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected DocumentFile configureDirectoryChooserIntent(Intent intent) {
|
||||||
|
super.configureDirectoryChooserIntent(intent);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||||
|
|
||||||
|
return PreferencesUtils.getDefaultExportDirectoryUri(this);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Intent createNextActivityIntent(Uri directoryUri) {
|
protected Intent createNextActivityIntent(Uri directoryUri) {
|
||||||
Intent intent = IntentUtils.newIntent(this, ExportActivity.class);
|
Intent intent = IntentUtils.newIntent(this, ExportActivity.class);
|
||||||
@@ -65,11 +90,44 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
|
|||||||
intent.putExtra(ExportActivity.EXTRA_TRACKFILEFORMAT_KEY, PreferencesUtils.getExportTrackFileFormat(this));
|
intent.putExtra(ExportActivity.EXTRA_TRACKFILEFORMAT_KEY, PreferencesUtils.getExportTrackFileFormat(this));
|
||||||
return intent;
|
return intent;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DefaultTrackExportDirectoryChooserActivity extends DirectoryChooserActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configureDirectoryChooserIntent(Intent intent) {
|
protected void onActivityResultCustom(int requestCode, int resultCode, @Nullable Intent resultData) {
|
||||||
|
if (requestCode == DIRECTORY_PICKER_REQUEST_CODE) {
|
||||||
|
switch (resultCode) {
|
||||||
|
case RESULT_OK:
|
||||||
|
Uri directoryUri = resultData.getData();
|
||||||
|
|
||||||
|
PreferencesUtils.setDefaultExportDirectoryUri(this, directoryUri);
|
||||||
|
IntentUtils.persistDirectoryAccessPermission(getApplicationContext(), resultData, directoryUri);
|
||||||
|
break;
|
||||||
|
case RESULT_CANCELED:
|
||||||
|
PreferencesUtils.setDefaultExportDirectoryUri(this, null);
|
||||||
|
//TODO Remove stored permission contentResolver.releasePersistableUriPermission
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected DocumentFile configureDirectoryChooserIntent(Intent intent) {
|
||||||
super.configureDirectoryChooserIntent(intent);
|
super.configureDirectoryChooserIntent(intent);
|
||||||
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||||
|
if (PreferencesUtils.isDefaultExportDirectoryUri(this)) {
|
||||||
|
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, PreferencesUtils.getDefaultExportDirectoryUri(this).getUri());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Intent createNextActivityIntent(Uri directoryUri) {
|
||||||
|
finish();
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
if (content != null) {
|
if (content != null) {
|
||||||
speed = content.trim();
|
speed = content.trim();
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
case TAG_EXTENSION_HEARTRATE:
|
case TAG_EXTENSION_HEARTRATE:
|
||||||
if (content != null) {
|
if (content != null) {
|
||||||
heartrate = content.trim();
|
heartrate = content.trim();
|
||||||
@@ -149,6 +150,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
|
|||||||
if (content != null) {
|
if (content != null) {
|
||||||
uuid = content.trim();
|
uuid = content.trim();
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset element content
|
// Reset element content
|
||||||
|
|||||||
@@ -16,59 +16,158 @@
|
|||||||
|
|
||||||
package de.dennisguse.opentracks.io.file.importer;
|
package de.dennisguse.opentracks.io.file.importer;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.fragments.ImportFinishedDialogFragment;
|
import de.dennisguse.opentracks.R;
|
||||||
import de.dennisguse.opentracks.fragments.ImportProgressDialogFragment;
|
import de.dennisguse.opentracks.databinding.ImportActivityBinding;
|
||||||
|
import de.dennisguse.opentracks.io.file.ErrorListDialog;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An activity to import files from the external storage.
|
* An activity to import files from the external storage.
|
||||||
*
|
*
|
||||||
* @author Rodrigo Damazio
|
* @author Rodrigo Damazio
|
||||||
*/
|
*/
|
||||||
public class ImportActivity extends FragmentActivity implements ImportProgressDialogFragment.DismissCallback, ImportFinishedDialogFragment.DismissCallback {
|
public class ImportActivity extends FragmentActivity {
|
||||||
|
|
||||||
private static final String TAG = ImportActivity.class.getSimpleName();
|
private static final String TAG = ImportActivity.class.getSimpleName();
|
||||||
|
|
||||||
public static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
|
public static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
|
||||||
|
|
||||||
private static final String BUNDLE_ACTIVITY_RECREATED = "activity_recreated";
|
private static final String BUNDLE_TOOLBAR_TITLE = "toolbar_title";
|
||||||
|
private static final String BUNDLE_DOCUMENT_URI = "document_uri";
|
||||||
|
private static final String BUNDLE_IS_DIRECTORY = "is_directory";
|
||||||
|
|
||||||
|
private ImportActivityBinding viewBinding;
|
||||||
|
|
||||||
|
boolean doubleBackToCancel = false;
|
||||||
|
|
||||||
|
private Uri documentUri;
|
||||||
|
private boolean isDirectory;
|
||||||
|
private String toolbarTitle;
|
||||||
|
|
||||||
|
private ImportViewModel viewModel;
|
||||||
|
private ImportViewModel.Summary summary;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
viewBinding = ImportActivityBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(viewBinding.getRoot());
|
||||||
|
|
||||||
if (savedInstanceState == null || !savedInstanceState.getBoolean(BUNDLE_ACTIVITY_RECREATED, false)) {
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
|
||||||
|
final DocumentFile documentFile;
|
||||||
|
if (savedInstanceState == null) {
|
||||||
if (getIntent().getData() != null) {
|
if (getIntent().getData() != null) {
|
||||||
ImportProgressDialogFragment.showDialog(getSupportFragmentManager(), getIntent().getData(), false);
|
documentUri = getIntent().getData();
|
||||||
|
isDirectory = false;
|
||||||
|
} else if (getIntent().getClipData() != null && getIntent().getClipData().getItemCount() > 0) {
|
||||||
|
documentUri = getIntent().getClipData().getItemAt(0).getUri();
|
||||||
|
isDirectory = false;
|
||||||
} else {
|
} else {
|
||||||
// Started from DirectoryChooserActivity
|
// Started from DirectoryChooserActivity
|
||||||
ImportProgressDialogFragment.showDialog(getSupportFragmentManager(), getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY), true);
|
documentUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
|
||||||
|
isDirectory = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
documentFile = isDirectory ? DocumentFile.fromTreeUri(this, documentUri) : DocumentFile.fromSingleUri(this, documentUri);
|
||||||
|
toolbarTitle = getString(R.string.import_progress_message, documentFile.getName());
|
||||||
|
} else {
|
||||||
|
documentUri = savedInstanceState.getParcelable(BUNDLE_DOCUMENT_URI);
|
||||||
|
toolbarTitle = savedInstanceState.getString(BUNDLE_TOOLBAR_TITLE);
|
||||||
|
isDirectory = savedInstanceState.getBoolean(BUNDLE_IS_DIRECTORY);
|
||||||
|
|
||||||
|
documentFile = isDirectory ? DocumentFile.fromTreeUri(this, documentUri) : DocumentFile.fromSingleUri(this, documentUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toolbar.setTitle(toolbarTitle);
|
||||||
|
initViews();
|
||||||
|
|
||||||
|
viewModel = new ViewModelProvider(this).get(ImportViewModel.class);
|
||||||
|
viewModel.getImportData(documentFile).observe(this, data -> {
|
||||||
|
summary = data;
|
||||||
|
setProgress();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putBoolean(BUNDLE_ACTIVITY_RECREATED, true);
|
outState.putParcelable(BUNDLE_DOCUMENT_URI, documentUri);
|
||||||
|
outState.putBoolean(BUNDLE_IS_DIRECTORY, isDirectory);
|
||||||
|
outState.putString(BUNDLE_TOOLBAR_TITLE, toolbarTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onImportCanceled(String directoryDisplayName, int successCount, int fileCount) {
|
public void onBackPressed() {
|
||||||
onImportFinished(directoryDisplayName, successCount, fileCount);
|
if (doubleBackToCancel || getTotalDone() == summary.getTotalCount()) {
|
||||||
|
super.onBackPressed();
|
||||||
|
viewModel.cancel();
|
||||||
|
getViewModelStore().clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.doubleBackToCancel = true;
|
||||||
|
Toast.makeText(this, getString(R.string.generic_click_twice_cancel), Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
|
new Handler().postDelayed(() -> doubleBackToCancel=false, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void initViews() {
|
||||||
public void onImportFinished(final String directoryDisplayName, final int successCount, final int fileCount) {
|
viewBinding.importProgressDone.setText("0");
|
||||||
runOnUiThread(() -> ImportFinishedDialogFragment.showDialog(getSupportFragmentManager(), directoryDisplayName, successCount, fileCount));
|
viewBinding.importProgressTotal.setText("0");
|
||||||
|
viewBinding.importProgressSummary.setText(getString(R.string.import_progress_review, 0, 0, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private int getTotalDone() {
|
||||||
public void onDismissed() {
|
return summary.getSuccessCount() + summary.getExistsCount() + summary.getErrorCount();
|
||||||
finish();
|
}
|
||||||
|
|
||||||
|
private void setProgress() {
|
||||||
|
int done = getTotalDone();
|
||||||
|
|
||||||
|
viewBinding.importProgressDone.setText("" + done);
|
||||||
|
viewBinding.importProgressTotal.setText("" + summary.getTotalCount());
|
||||||
|
|
||||||
|
viewBinding.importProgressBar.setProgress((int) ((float) done / (float) summary.getTotalCount() * 100f));
|
||||||
|
viewBinding.importProgressSummary.setText(getString(R.string.import_progress_review, getTotalDone(), summary.getSuccessCount(), summary.getExistsCount(), summary.getErrorCount()));
|
||||||
|
|
||||||
|
if (done == summary.getTotalCount()) {
|
||||||
|
onImportEnded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onImportEnded() {
|
||||||
|
viewBinding.importProgressRightButton.setVisibility(View.VISIBLE);
|
||||||
|
viewBinding.importProgressRightButton.setText(getString(R.string.generic_ok));
|
||||||
|
viewBinding.importProgressRightButton.setOnClickListener((view) -> {
|
||||||
|
getViewModelStore().clear();
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
viewBinding.importProgressAlertMsg.setVisibility(View.VISIBLE);
|
||||||
|
viewBinding.importProgressAlertIcon.setVisibility(View.VISIBLE);
|
||||||
|
if (summary.getErrorCount() > 0) {
|
||||||
|
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));
|
||||||
|
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.importProgressAlertMsg.setText(getString(R.string.generic_completed));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.dennisguse.opentracks.io.file.importer;
|
||||||
|
|
||||||
|
class ImportAlreadyExistsException extends RuntimeException {
|
||||||
|
public ImportAlreadyExistsException(Exception e) {
|
||||||
|
super(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package de.dennisguse.opentracks.io.file.importer;
|
||||||
|
|
||||||
|
class ImportParserException extends RuntimeException {
|
||||||
|
public ImportParserException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportParserException(Exception e) {
|
||||||
|
super(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package de.dennisguse.opentracks.io.file.importer;
|
||||||
|
|
||||||
|
import android.app.job.JobService;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.ResultReceiver;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.app.JobIntentService;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.R;
|
||||||
|
import de.dennisguse.opentracks.content.data.Track;
|
||||||
|
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||||
|
import de.dennisguse.opentracks.util.FileUtils;
|
||||||
|
|
||||||
|
public class ImportService extends JobIntentService {
|
||||||
|
|
||||||
|
private static final String TAG = ImportService.class.getSimpleName();
|
||||||
|
|
||||||
|
private static final int JOB_ID = 2;
|
||||||
|
|
||||||
|
private static final String EXTRA_RECEIVER = "extra_receiver";
|
||||||
|
private static final String EXTRA_URI = "extra_uri";
|
||||||
|
|
||||||
|
private ResultReceiver resultReceiver;
|
||||||
|
|
||||||
|
public static void enqueue(Context context, ImportServiceResultReceiver receiver, Uri uri) {
|
||||||
|
Intent intent = new Intent(context, JobService.class);
|
||||||
|
intent.putExtra(EXTRA_RECEIVER, receiver);
|
||||||
|
intent.putExtra(EXTRA_URI, uri);
|
||||||
|
enqueueWork(context, ImportService.class, JOB_ID, intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHandleWork(@NonNull Intent intent) {
|
||||||
|
resultReceiver = intent.getParcelableExtra(EXTRA_RECEIVER);
|
||||||
|
Uri uri = intent.getParcelableExtra(EXTRA_URI);
|
||||||
|
importFile(DocumentFile.fromSingleUri(this, uri));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importFile(DocumentFile file) {
|
||||||
|
TrackImporter trackImporter;
|
||||||
|
String fileExtension = FileUtils.getExtension(file);
|
||||||
|
|
||||||
|
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
|
||||||
|
trackImporter = new GpxFileTrackImporter(this);
|
||||||
|
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
|
||||||
|
trackImporter = new KmlFileTrackImporter(this);
|
||||||
|
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
|
||||||
|
trackImporter = new KmzTrackImporter(this, file.getUri());
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Unsupported file format.");
|
||||||
|
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file.getName(), getString(R.string.import_unsupported_format));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (InputStream inputStream = getContentResolver().openInputStream(file.getUri())) {
|
||||||
|
Track.Id trackId = trackImporter.importFile(inputStream);
|
||||||
|
if (trackId != null) {
|
||||||
|
sendResult(ImportServiceResultReceiver.RESULT_CODE_IMPORTED, trackId, file.getName(), getString(R.string.import_file_imported, file.getName()));
|
||||||
|
} else {
|
||||||
|
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, trackId, file.getName(), getString(R.string.import_unable_to_import_file, file.getName()));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to import file", e);
|
||||||
|
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file.getName(), getString(R.string.import_unable_to_import_file, e.getMessage()));
|
||||||
|
} catch (ImportParserException e) {
|
||||||
|
Log.d(TAG, "Parser error: " + e.getMessage(), e);
|
||||||
|
sendResult(ImportServiceResultReceiver.RESULT_CODE_ERROR, null, file.getName(), getString(R.string.import_parser_error, e.getMessage()));
|
||||||
|
} catch (ImportAlreadyExistsException e) {
|
||||||
|
Log.d(TAG, "Track already exists: " + e.getMessage(), e);
|
||||||
|
sendResult(ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS, null, file.getName(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendResult(int resultCode, Track.Id trackId, String fileName, String message) {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putParcelable(ImportServiceResultReceiver.RESULT_EXTRA_TRACK_ID, trackId);
|
||||||
|
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME, fileName);
|
||||||
|
bundle.putString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE, message);
|
||||||
|
resultReceiver.send(resultCode, bundle);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package de.dennisguse.opentracks.io.file.importer;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.ResultReceiver;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ResultReceive to receive results.
|
||||||
|
* Your {@link #onReceiveResult} method will be called from the thread running <var>handler</var> if given, or from an arbitrary thread if null.
|
||||||
|
*/
|
||||||
|
public class ImportServiceResultReceiver extends ResultReceiver {
|
||||||
|
|
||||||
|
public static final int RESULT_CODE_ERROR = 0;
|
||||||
|
public static final int RESULT_CODE_IMPORTED = 1;
|
||||||
|
public static final int RESULT_CODE_ALREADY_EXISTS = 2;
|
||||||
|
|
||||||
|
public static final String RESULT_EXTRA_TRACK_ID = "result_track_id";
|
||||||
|
public static final String RESULT_EXTRA_FILENAME = "result_extra_filename";
|
||||||
|
public static final String RESULT_EXTRA_MESSAGE = "result_extra_message";
|
||||||
|
|
||||||
|
private final Receiver receiver;
|
||||||
|
|
||||||
|
public ImportServiceResultReceiver(Handler handler, @NonNull Receiver receiver) {
|
||||||
|
super(handler);
|
||||||
|
this.receiver = receiver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
||||||
|
receiver.onReceiveResult(resultCode, resultData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Receiver {
|
||||||
|
void onReceiveResult(int resultCode, Bundle resultData);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package de.dennisguse.opentracks.io.file.importer;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import de.dennisguse.opentracks.R;
|
||||||
|
import de.dennisguse.opentracks.util.FileUtils;
|
||||||
|
|
||||||
|
public class ImportViewModel extends AndroidViewModel implements ImportServiceResultReceiver.Receiver {
|
||||||
|
|
||||||
|
private static final String TAG = ImportViewModel.class.getSimpleName();
|
||||||
|
|
||||||
|
private MutableLiveData<Summary> importData;
|
||||||
|
private final ImportServiceResultReceiver resultReceiver;
|
||||||
|
private final Summary summary;
|
||||||
|
private boolean cancel = false;
|
||||||
|
private final List<DocumentFile> filesToImport = new ArrayList<>();
|
||||||
|
|
||||||
|
public ImportViewModel(@NonNull Application application) {
|
||||||
|
super(application);
|
||||||
|
resultReceiver = new ImportServiceResultReceiver(new Handler(), this);
|
||||||
|
summary = new Summary();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Summary> getImportData(DocumentFile documentFile) {
|
||||||
|
if (importData == null) {
|
||||||
|
importData = new MutableLiveData<>();
|
||||||
|
loadData(documentFile);
|
||||||
|
}
|
||||||
|
return importData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
cancel = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadData(DocumentFile documentFile) {
|
||||||
|
List<DocumentFile> fileList = FileUtils.getFiles(documentFile);
|
||||||
|
summary.totalCount = fileList.size();
|
||||||
|
filesToImport.addAll(fileList);
|
||||||
|
importNextFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importNextFile() {
|
||||||
|
if (cancel || filesToImport.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ImportService.enqueue(getApplication(), resultReceiver, filesToImport.get(0).getUri());
|
||||||
|
filesToImport.remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceiveResult(int resultCode, Bundle resultData) {
|
||||||
|
if (resultData == null) {
|
||||||
|
throw new RuntimeException(TAG + ": onReceiveResult resultData NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
String fileName = resultData.getString(ImportServiceResultReceiver.RESULT_EXTRA_FILENAME);
|
||||||
|
String message = resultData.getString(ImportServiceResultReceiver.RESULT_EXTRA_MESSAGE);
|
||||||
|
|
||||||
|
switch (resultCode) {
|
||||||
|
case ImportServiceResultReceiver.RESULT_CODE_ERROR:
|
||||||
|
summary.errorCount++;
|
||||||
|
summary.fileErrors.add(getApplication().getString(R.string.import_error_info, fileName, message));
|
||||||
|
break;
|
||||||
|
case ImportServiceResultReceiver.RESULT_CODE_IMPORTED:
|
||||||
|
summary.successCount++;
|
||||||
|
break;
|
||||||
|
case ImportServiceResultReceiver.RESULT_CODE_ALREADY_EXISTS:
|
||||||
|
summary.existsCount++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new RuntimeException(TAG + ": import service result code invalid: " + resultCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
importData.postValue(summary);
|
||||||
|
importNextFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Summary {
|
||||||
|
private int totalCount;
|
||||||
|
private int successCount;
|
||||||
|
private int existsCount;
|
||||||
|
private int errorCount;
|
||||||
|
private final ArrayList<String> fileErrors = new ArrayList<>();
|
||||||
|
|
||||||
|
public int getTotalCount() {
|
||||||
|
return totalCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSuccessCount() {
|
||||||
|
return successCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getExistsCount() {
|
||||||
|
return existsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getErrorCount() {
|
||||||
|
return errorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArrayList<String> getFileErrors() {
|
||||||
|
return fileErrors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,7 +74,13 @@ public class KmzTrackImporter implements TrackImporter {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
trackId = findAndParseKmlFile(inputStream);
|
try {
|
||||||
|
trackId = findAndParseKmlFile(inputStream);
|
||||||
|
} catch (Exception e) {
|
||||||
|
cleanImport(context, importTrackId);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
if (trackId == null) {
|
if (trackId == null) {
|
||||||
cleanImport(context, importTrackId);
|
cleanImport(context, importTrackId);
|
||||||
return null;
|
return null;
|
||||||
@@ -175,7 +181,7 @@ public class KmzTrackImporter implements TrackImporter {
|
|||||||
String fileName = zipEntry.getName();
|
String fileName = zipEntry.getName();
|
||||||
if (KmzTrackExporter.KMZ_KML_FILE.equals(fileName)) {
|
if (KmzTrackExporter.KMZ_KML_FILE.equals(fileName)) {
|
||||||
trackId = parseKml(zipInputStream);
|
trackId = parseKml(zipInputStream);
|
||||||
if (trackId != null) {
|
if (trackId == null) {
|
||||||
Log.d(TAG, "Unable to parse kml in kmz");
|
Log.d(TAG, "Unable to parse kml in kmz");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -184,9 +190,12 @@ public class KmzTrackImporter implements TrackImporter {
|
|||||||
zipInputStream.closeEntry();
|
zipInputStream.closeEntry();
|
||||||
}
|
}
|
||||||
return trackId;
|
return trackId;
|
||||||
|
} catch (ImportParserException | ImportAlreadyExistsException e) {
|
||||||
|
Log.e(TAG, "Unable to import file", e);
|
||||||
|
throw e;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, "Unable to import file", e);
|
Log.e(TAG, "Unable to import file", e);
|
||||||
return null;
|
throw new ImportParserException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,11 +251,15 @@ public class KmzTrackImporter implements TrackImporter {
|
|||||||
* @param zipInputStream the zip input stream
|
* @param zipInputStream the zip input stream
|
||||||
* @return the imported track id or -1L
|
* @return the imported track id or -1L
|
||||||
*/
|
*/
|
||||||
private Track.Id parseKml(ZipInputStream zipInputStream) throws IOException {
|
private Track.Id parseKml(ZipInputStream zipInputStream) {
|
||||||
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context);
|
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context);
|
||||||
|
|
||||||
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream))) {
|
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream))) {
|
||||||
return kmlFileTrackImporter.importFile(byteArrayInputStream);
|
return kmlFileTrackImporter.importFile(byteArrayInputStream);
|
||||||
|
} catch (ImportParserException | ImportAlreadyExistsException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new ImportParserException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory;
|
|||||||
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
|
import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor;
|
||||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||||
|
import de.dennisguse.opentracks.util.ExportUtils;
|
||||||
import de.dennisguse.opentracks.util.IntentUtils;
|
import de.dennisguse.opentracks.util.IntentUtils;
|
||||||
import de.dennisguse.opentracks.util.LocationUtils;
|
import de.dennisguse.opentracks.util.LocationUtils;
|
||||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||||
@@ -186,6 +187,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
|
|
||||||
PreferencesUtils.unregister(this, sharedPreferenceChangeListener);
|
PreferencesUtils.unregister(this, sharedPreferenceChangeListener);
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
voiceExecutor.shutdown();
|
voiceExecutor.shutdown();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -428,6 +430,9 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
updateTrackTotalTime(track);
|
updateTrackTotalTime(track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExportUtils.postWorkoutExport(this, track);
|
||||||
|
|
||||||
endRecording(true);
|
endRecording(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -684,7 +689,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
|
|||||||
}
|
}
|
||||||
|
|
||||||
SensorDataSet getSensorDataSet() {
|
SensorDataSet getSensorDataSet() {
|
||||||
if (remoteSensorManager == null || !remoteSensorManager.isEnabled()) {
|
if (remoteSensorManager == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ public abstract class BluetoothConnectionManager {
|
|||||||
switch (newState) {
|
switch (newState) {
|
||||||
case BluetoothProfile.STATE_CONNECTING:
|
case BluetoothProfile.STATE_CONNECTING:
|
||||||
Log.d(TAG, "Connecting to sensor: " + gatt.getDevice());
|
Log.d(TAG, "Connecting to sensor: " + gatt.getDevice());
|
||||||
|
break;
|
||||||
case BluetoothProfile.STATE_CONNECTED:
|
case BluetoothProfile.STATE_CONNECTED:
|
||||||
Log.d(TAG, "Connected to sensor: " + gatt.getDevice());
|
Log.d(TAG, "Connected to sensor: " + gatt.getDevice());
|
||||||
|
|
||||||
@@ -62,9 +63,11 @@ public abstract class BluetoothConnectionManager {
|
|||||||
break;
|
break;
|
||||||
case BluetoothProfile.STATE_DISCONNECTING:
|
case BluetoothProfile.STATE_DISCONNECTING:
|
||||||
Log.d(TAG, "Disconnecting from sensor: " + gatt.getDevice());
|
Log.d(TAG, "Disconnecting from sensor: " + gatt.getDevice());
|
||||||
|
break;
|
||||||
|
|
||||||
case BluetoothProfile.STATE_DISCONNECTED:
|
case BluetoothProfile.STATE_DISCONNECTED:
|
||||||
Log.d(TAG, "Disconnected from sensor: " + gatt.getDevice());
|
Log.d(TAG, "Disconnected from sensor: " + gatt.getDevice());
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,8 +117,9 @@ public abstract class BluetoothConnectionManager {
|
|||||||
|
|
||||||
Log.d(TAG, "Connecting to: " + device);
|
Log.d(TAG, "Connecting to: " + device);
|
||||||
|
|
||||||
bluetoothGatt = device.connectGatt(context, true, this.connectCallback);
|
bluetoothGatt = device.connectGatt(context, true, connectCallback);
|
||||||
|
SensorData sensorData = createEmptySensorData(bluetoothGatt.getDevice().getAddress());
|
||||||
|
observer.onChanged(sensorData);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void disconnect() {
|
synchronized void disconnect() {
|
||||||
@@ -124,6 +128,8 @@ public abstract class BluetoothConnectionManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bluetoothGatt.close();
|
bluetoothGatt.close();
|
||||||
|
observer.onDisconnecting(createEmptySensorData(bluetoothGatt.getDevice().getAddress()));
|
||||||
|
|
||||||
bluetoothGatt = null;
|
bluetoothGatt = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,10 +141,12 @@ public abstract class BluetoothConnectionManager {
|
|||||||
return address.equals(bluetoothGatt.getDevice().getAddress());
|
return address.equals(bluetoothGatt.getDevice().getAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract SensorData createEmptySensorData(String address);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return null if data could not be parsed.
|
* @return null if data could not be parsed.
|
||||||
*/
|
*/
|
||||||
protected abstract de.dennisguse.opentracks.content.sensor.SensorData parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic);
|
protected abstract SensorData parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic);
|
||||||
|
|
||||||
public static class HeartRate extends BluetoothConnectionManager {
|
public static class HeartRate extends BluetoothConnectionManager {
|
||||||
|
|
||||||
@@ -146,6 +154,11 @@ public abstract class BluetoothConnectionManager {
|
|||||||
super(BluetoothUtils.HEART_RATE_SERVICE_UUID, BluetoothUtils.HEART_RATE_MEASUREMENT_CHAR_UUID, observer);
|
super(BluetoothUtils.HEART_RATE_SERVICE_UUID, BluetoothUtils.HEART_RATE_MEASUREMENT_CHAR_UUID, observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SensorData createEmptySensorData(String address) {
|
||||||
|
return new SensorDataHeartRate(address);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected SensorDataHeartRate parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
protected SensorDataHeartRate parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||||
Integer heartRate = BluetoothUtils.parseHeartRate(characteristic);
|
Integer heartRate = BluetoothUtils.parseHeartRate(characteristic);
|
||||||
@@ -160,6 +173,11 @@ public abstract class BluetoothConnectionManager {
|
|||||||
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
|
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SensorData createEmptySensorData(String address) {
|
||||||
|
return new SensorDataCycling.Cadence(address);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected SensorDataCycling.Cadence parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
protected SensorDataCycling.Cadence parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||||
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
|
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
|
||||||
@@ -186,6 +204,11 @@ public abstract class BluetoothConnectionManager {
|
|||||||
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
|
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPPED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SensorData createEmptySensorData(String address) {
|
||||||
|
return new SensorDataCycling.Speed(address);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected SensorDataCycling.Speed parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
protected SensorDataCycling.Speed parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
|
||||||
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
|
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
|
||||||
@@ -200,5 +223,6 @@ public abstract class BluetoothConnectionManager {
|
|||||||
|
|
||||||
void onChanged(SensorData sensorData);
|
void onChanged(SensorData sensorData);
|
||||||
|
|
||||||
|
void onDisconnecting(SensorData sensorData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
|
|||||||
|
|
||||||
if (PreferencesUtils.isBluetoothSensorAddressNone(context, address)) {
|
if (PreferencesUtils.isBluetoothSensorAddressNone(context, address)) {
|
||||||
Log.w(TAG, "No Bluetooth address.");
|
Log.w(TAG, "No Bluetooth address.");
|
||||||
|
connectionManager.disconnect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,4 +169,9 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
|
|||||||
|
|
||||||
sensorDataSet.set(sensorData);
|
sensorDataSet.set(sensorData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnecting(SensorData sensorData) {
|
||||||
|
sensorDataSet.remove(sensorData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ public class ActivityTypePreference extends DialogPreference {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getDialogLayoutResource() {
|
public int getDialogLayoutResource() {
|
||||||
|
// TODO PreferenceActivityTypeBinding
|
||||||
return R.layout.preference_activity_type;
|
return R.layout.preference_activity_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.widget.Toast;
|
|||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.preference.EditTextPreference;
|
import androidx.preference.EditTextPreference;
|
||||||
import androidx.preference.ListPreference;
|
import androidx.preference.ListPreference;
|
||||||
@@ -88,6 +89,13 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
|
|||||||
|
|
||||||
setExportTrackFileFormatOptions();
|
setExportTrackFileFormatOptions();
|
||||||
setWheelCircumferenceInputFilter();
|
setWheelCircumferenceInputFilter();
|
||||||
|
|
||||||
|
Preference instantExportDirectoryPreference = findPreference(getString(R.string.settings_default_export_directory_key));
|
||||||
|
instantExportDirectoryPreference.setSummaryProvider(preference -> {
|
||||||
|
DocumentFile directory = PreferencesUtils.getDefaultExportDirectoryUri(getContext());
|
||||||
|
//Use same value for not set as Androidx ListPreference and EditTextPreference
|
||||||
|
return directory != null ? directory.getName() : getString(R.string.not_set);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -97,6 +105,13 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
|
|||||||
updateUnits();
|
updateUnits();
|
||||||
updateReset();
|
updateReset();
|
||||||
updateBluetooth();
|
updateBluetooth();
|
||||||
|
|
||||||
|
updatePostWorkoutExport();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePostWorkoutExport() {
|
||||||
|
Preference instantExportEnabledPreference = findPreference(getString(R.string.post_workout_export_enabled_key));
|
||||||
|
instantExportEnabledPreference.setEnabled(PreferencesUtils.isDefaultExportDirectoryUri(getContext()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -206,4 +221,5 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
package de.dennisguse.opentracks.util;
|
package de.dennisguse.opentracks.util;
|
||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
|
||||||
@@ -53,36 +52,4 @@ public class DialogUtils {
|
|||||||
.setTitle(titleId).create();
|
.setTitle(titleId).create();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a horizontal progress dialog.
|
|
||||||
*
|
|
||||||
* @param context the context
|
|
||||||
* @param messageId the progress message id
|
|
||||||
* @param onCancelListener the cancel listener
|
|
||||||
* @param formatArgs the format arguments for the messageId
|
|
||||||
*/
|
|
||||||
public static ProgressDialog createHorizontalProgressDialog(Context context, int messageId, DialogInterface.OnCancelListener onCancelListener, Object... formatArgs) {
|
|
||||||
return createProgressDialog(context, messageId, onCancelListener, formatArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a progress dialog.
|
|
||||||
*
|
|
||||||
* @param context the context
|
|
||||||
* @param messageId the progress message id
|
|
||||||
* @param onCancelListener the cancel listener
|
|
||||||
* @param formatArgs the format arguments for the message id
|
|
||||||
*/
|
|
||||||
private static ProgressDialog createProgressDialog(final Context context, int messageId, DialogInterface.OnCancelListener onCancelListener, Object... formatArgs) {
|
|
||||||
final ProgressDialog progressDialog = new ProgressDialog(context, R.style.ThemeCustomNotTransparentDialog);
|
|
||||||
progressDialog.setCancelable(true);
|
|
||||||
progressDialog.setCanceledOnTouchOutside(false);
|
|
||||||
progressDialog.setIcon(R.drawable.ic_dialog_info_24dp);
|
|
||||||
progressDialog.setIndeterminate(true);
|
|
||||||
progressDialog.setMessage(context.getString(messageId, formatArgs));
|
|
||||||
progressDialog.setOnCancelListener(onCancelListener);
|
|
||||||
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
|
||||||
progressDialog.setTitle(R.string.generic_progress_title);
|
|
||||||
return progressDialog;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package de.dennisguse.opentracks.util;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
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.TrackExporter;
|
||||||
|
|
||||||
|
public class ExportUtils {
|
||||||
|
|
||||||
|
private static final String TAG = ExportUtils.class.getSimpleName();
|
||||||
|
|
||||||
|
public static void postWorkoutExport(Context context, Track track) {
|
||||||
|
if (PreferencesUtils.shouldInstantExportAfterWorkout(context)) {
|
||||||
|
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(context);
|
||||||
|
DocumentFile directory = PreferencesUtils.getDefaultExportDirectoryUri(context);
|
||||||
|
|
||||||
|
exportTrack(context, trackFileFormat, directory, track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean exportTrack(Context context, TrackFileFormat trackFileFormat, DocumentFile directory, Track track) {
|
||||||
|
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context);
|
||||||
|
|
||||||
|
Uri exportDocumentFileUri = getExportDocumentFileUri(context, track.getId(), trackFileFormat, directory);
|
||||||
|
|
||||||
|
try (OutputStream outputStream = context.getContentResolver().openOutputStream(exportDocumentFileUri)) {
|
||||||
|
if (trackExporter.writeTrack(track, outputStream)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if (!DocumentFile.fromSingleUri(context, exportDocumentFileUri).delete()) {
|
||||||
|
Log.e(TAG, "Unable to delete exportDocumentFile");
|
||||||
|
}
|
||||||
|
Log.e(TAG, "Unable to export track");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
Log.e(TAG, "Unable to open exportDocumentFile " + exportDocumentFileUri, e);
|
||||||
|
return false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Unable to close exportDocumentFile output stream", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isExportFileExists(Track.Id trackId, String trackFileFormatExtension, List<String> filesName) {
|
||||||
|
return filesName.contains(getExportFileNameByTrackId(trackId, trackFileFormatExtension));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<String> getAllFiles(Context context, Uri directoryUri) {
|
||||||
|
List<String> fileNames = new ArrayList<>();
|
||||||
|
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
|
||||||
|
);
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
fileNames.add(c.getString(0));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Failed query: " + e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Uri getExportDocumentFileUri(Context context, Track.Id trackId, TrackFileFormat trackFileFormat, DocumentFile directory) {
|
||||||
|
String exportFileName = getExportFileNameByTrackId(trackId, trackFileFormat.getExtension());
|
||||||
|
Uri exportDocumentFileUri = findFile(context, directory.getUri(), exportFileName);
|
||||||
|
if (exportDocumentFileUri == null) {
|
||||||
|
exportDocumentFileUri = directory.createFile(trackFileFormat.getMimeType(), exportFileName).getUri();
|
||||||
|
}
|
||||||
|
return exportDocumentFileUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getExportFileNameByTrackId(Track.Id trackId, String trackFileFormatExtension) {
|
||||||
|
return trackId.getId() + "." + trackFileFormatExtension;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Uri findFile(Context context, Uri directoryUri, String exportFileName) {
|
||||||
|
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
|
||||||
|
);
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
final String documentId = c.getString(0);
|
||||||
|
final String documentName = c.getString(1);
|
||||||
|
if (documentName.equals(exportFileName)) {
|
||||||
|
return DocumentsContract.buildDocumentUriUsingTree(directoryUri, documentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Find file error: failed query: " + e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ public class HackUtils {
|
|||||||
*/
|
*/
|
||||||
public static void invalidatePreference(@NonNull Preference preference) {
|
public static void invalidatePreference(@NonNull Preference preference) {
|
||||||
boolean isEnabled = preference.isEnabled();
|
boolean isEnabled = preference.isEnabled();
|
||||||
preference.setEnabled(!isEnabled);
|
preference.setVisible(!isEnabled);
|
||||||
preference.setEnabled(isEnabled);
|
preference.setVisible(isEnabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,4 +173,11 @@ public class IntentUtils {
|
|||||||
.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
|
.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
|
||||||
return new Pair<>(intent, photoUri);
|
return new Pair<>(intent, photoUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void persistDirectoryAccessPermission(Context context, Intent resultData, Uri directoryUri) {
|
||||||
|
final int takeFlags = resultData.getFlags()
|
||||||
|
& (Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||||
|
context.getContentResolver().takePersistableUriPermission(directoryUri, takeFlags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ package de.dennisguse.opentracks.util;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.SharedPreferences.Editor;
|
import android.content.SharedPreferences.Editor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.R;
|
import de.dennisguse.opentracks.R;
|
||||||
@@ -34,6 +37,8 @@ import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
|||||||
*/
|
*/
|
||||||
public class PreferencesUtils {
|
public class PreferencesUtils {
|
||||||
|
|
||||||
|
private final static String TAG = PreferencesUtils.class.getSimpleName();
|
||||||
|
|
||||||
private PreferencesUtils() {
|
private PreferencesUtils() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,29 +245,14 @@ public class PreferencesUtils {
|
|||||||
return getBluetoothSensorAddressNone(context).equals(currentValue);
|
return getBluetoothSensorAddressNone(context).equals(currentValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static boolean isBluetoothHeartRateSensorAddressNone(Context context) {
|
|
||||||
return isBluetoothSensorAddressNone(context, getBluetoothHeartRateSensorAddress(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getBluetoothHeartRateSensorAddress(Context context) {
|
public static String getBluetoothHeartRateSensorAddress(Context context) {
|
||||||
return getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothSensorAddressNone(context));
|
return getString(context, R.string.settings_sensor_bluetooth_heart_rate_key, getBluetoothSensorAddressNone(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static boolean isBluetoothCyclingCadenceSensorAddressNone(Context context) {
|
|
||||||
return isBluetoothSensorAddressNone(context, getBluetoothCyclingCadenceSensorAddress(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getBluetoothCyclingCadenceSensorAddress(Context context) {
|
public static String getBluetoothCyclingCadenceSensorAddress(Context context) {
|
||||||
return getString(context, R.string.settings_sensor_bluetooth_cycling_cadence_key, getBluetoothSensorAddressNone(context));
|
return getString(context, R.string.settings_sensor_bluetooth_cycling_cadence_key, getBluetoothSensorAddressNone(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static boolean isBluetoothCyclingSpeedSensorAddressNone(Context context) {
|
|
||||||
return isBluetoothSensorAddressNone(context, getBluetoothCyclingSpeedSensorAddress(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getBluetoothCyclingSpeedSensorAddress(Context context) {
|
public static String getBluetoothCyclingSpeedSensorAddress(Context context) {
|
||||||
return getString(context, R.string.settings_sensor_bluetooth_cycling_speed_key, getBluetoothSensorAddressNone(context));
|
return getString(context, R.string.settings_sensor_bluetooth_cycling_speed_key, getBluetoothSensorAddressNone(context));
|
||||||
}
|
}
|
||||||
@@ -337,6 +327,11 @@ public class PreferencesUtils {
|
|||||||
return getInt(context, R.string.recording_gps_accuracy_key, RECORDING_GPS_ACCURACY);
|
return getInt(context, R.string.recording_gps_accuracy_key, RECORDING_GPS_ACCURACY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean shouldInstantExportAfterWorkout(Context context) {
|
||||||
|
final boolean INSTANT_POST_WORKOUT_EXPORT_DEFAULT = context.getResources().getBoolean(R.bool.post_workout_export_enabled_default);
|
||||||
|
return getBoolean(context, R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
public static TrackFileFormat getExportTrackFileFormat(Context context) {
|
public static TrackFileFormat getExportTrackFileFormat(Context context) {
|
||||||
final String TRACKFILEFORMAT_NAME_DEFAULT = getString(context, R.string.export_trackfileformat_default, null);
|
final String TRACKFILEFORMAT_NAME_DEFAULT = getString(context, R.string.export_trackfileformat_default, null);
|
||||||
String trackFileFormatName = getString(context, R.string.export_trackfileformat_key, TRACKFILEFORMAT_NAME_DEFAULT);
|
String trackFileFormatName = getString(context, R.string.export_trackfileformat_key, TRACKFILEFORMAT_NAME_DEFAULT);
|
||||||
@@ -383,4 +378,26 @@ public class PreferencesUtils {
|
|||||||
}
|
}
|
||||||
PreferenceManager.setDefaultValues(context, R.xml.settings, readAgain);
|
PreferenceManager.setDefaultValues(context, R.xml.settings, readAgain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static DocumentFile getDefaultExportDirectoryUri(Context context) {
|
||||||
|
String singleExportDirectorySettingsKey = getString(context, R.string.settings_default_export_directory_key, null);
|
||||||
|
if (singleExportDirectorySettingsKey == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return DocumentFile.fromTreeUri(context, Uri.parse(singleExportDirectorySettingsKey));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Could not decode default export directory: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setDefaultExportDirectoryUri(Context context, Uri directoryUri) {
|
||||||
|
String value = directoryUri != null ? directoryUri.toString() : null;
|
||||||
|
setString(context, R.string.settings_default_export_directory_key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isDefaultExportDirectoryUri(Context context) {
|
||||||
|
return getDefaultExportDirectoryUri(context) != null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,8 +154,8 @@ public class TrackIconUtils {
|
|||||||
* @param context the context
|
* @param context the context
|
||||||
* @param activityType the activity type
|
* @param activityType the activity type
|
||||||
*/
|
*/
|
||||||
public static @NonNull
|
@NonNull
|
||||||
String getIconValue(Context context, String activityType) {
|
public static String getIconValue(Context context, String activityType) {
|
||||||
if (activityType == null || activityType.equals("")) {
|
if (activityType == null || activityType.equals("")) {
|
||||||
return UNKNOWN;
|
return UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class IntervalStatistics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Interval interval = new Interval();
|
Interval interval = new Interval();
|
||||||
|
interval.gain_m += trackPointList.get(0).hasElevationGain() ? trackPointList.get(0).getElevationGain() : 0;
|
||||||
for (int i = 1; i < trackPointList.size(); i++) {
|
for (int i = 1; i < trackPointList.size(); i++) {
|
||||||
TrackPoint prevTrackPoint = trackPointList.get(i - 1);
|
TrackPoint prevTrackPoint = trackPointList.get(i - 1);
|
||||||
TrackPoint trackPoint = trackPointList.get(i);
|
TrackPoint trackPoint = trackPointList.get(i);
|
||||||
@@ -31,6 +32,7 @@ public class IntervalStatistics {
|
|||||||
if (LocationUtils.isValidLocation(trackPoint.getLocation()) && LocationUtils.isValidLocation(prevTrackPoint.getLocation())) {
|
if (LocationUtils.isValidLocation(trackPoint.getLocation()) && LocationUtils.isValidLocation(prevTrackPoint.getLocation())) {
|
||||||
interval.distance_m += prevTrackPoint.distanceTo(trackPoint);
|
interval.distance_m += prevTrackPoint.distanceTo(trackPoint);
|
||||||
interval.time_ms += trackPoint.getTime() - prevTrackPoint.getTime();
|
interval.time_ms += trackPoint.getTime() - prevTrackPoint.getTime();
|
||||||
|
interval.gain_m += trackPoint.hasElevationGain() ? trackPoint.getElevationGain() : 0;
|
||||||
|
|
||||||
if (interval.distance_m >= distanceInterval_m) {
|
if (interval.distance_m >= distanceInterval_m) {
|
||||||
float adjustFactor = distanceInterval_m / interval.distance_m;
|
float adjustFactor = distanceInterval_m / interval.distance_m;
|
||||||
@@ -80,17 +82,20 @@ public class IntervalStatistics {
|
|||||||
public static class Interval {
|
public static class Interval {
|
||||||
private float distance_m = 0f;
|
private float distance_m = 0f;
|
||||||
private float time_ms = 0f;
|
private float time_ms = 0f;
|
||||||
|
private float gain_m = 0f;
|
||||||
|
|
||||||
public Interval() {}
|
public Interval() {}
|
||||||
|
|
||||||
public Interval(float distance_m, float time_ms) {
|
public Interval(float distance_m, float time_ms) {
|
||||||
this.distance_m = distance_m;
|
this.distance_m = distance_m;
|
||||||
this.time_ms = time_ms;
|
this.time_ms = time_ms;
|
||||||
|
this.gain_m = 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Interval(Interval i) {
|
public Interval(Interval i) {
|
||||||
distance_m = i.distance_m;
|
distance_m = i.distance_m;
|
||||||
time_ms = i.time_ms;
|
time_ms = i.time_ms;
|
||||||
|
gain_m = i.gain_m;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float getDistance_m() {
|
public float getDistance_m() {
|
||||||
@@ -111,5 +116,9 @@ public class IntervalStatistics {
|
|||||||
}
|
}
|
||||||
return distance_m / (float) (time_ms * UnitConversions.MS_TO_S);
|
return distance_m / (float) (time_ms * UnitConversions.MS_TO_S);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public float getGain_m() {
|
||||||
|
return gain_m;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,45 @@
|
|||||||
package de.dennisguse.opentracks.viewmodels;
|
package de.dennisguse.opentracks.viewmodels;
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.lifecycle.MutableLiveData;
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import de.dennisguse.opentracks.content.data.Track;
|
|
||||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
|
||||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
|
||||||
import de.dennisguse.opentracks.util.UnitConversions;
|
import de.dennisguse.opentracks.util.UnitConversions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This view model is used to load intervals for a track.
|
* This model is used to load intervals for a track.
|
||||||
* It uses a default interval but it can be set from outside to manage the interval length.
|
* It uses a default interval but it can be set from outside to manage the interval length.
|
||||||
*/
|
*/
|
||||||
public class IntervalStatisticsModel extends AndroidViewModel {
|
public class IntervalStatisticsModel {
|
||||||
|
|
||||||
private MutableLiveData<IntervalStatistics> intervalStats = new MutableLiveData<>();
|
private IntervalStatistics intervalStats;
|
||||||
|
private final List<TrackPoint> trackPoints = new ArrayList<>();
|
||||||
|
|
||||||
public IntervalStatisticsModel(@NonNull Application application) {
|
public IntervalStatistics getIntervalStats(boolean metricUnits, @Nullable IntervalOption interval) {
|
||||||
super(application);
|
synchronized (trackPoints) {
|
||||||
|
if (interval == null) {
|
||||||
|
interval = IntervalOption.OPTION_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
intervalStats = new IntervalStatistics();
|
||||||
|
float distanceInterval = metricUnits ? (float) (interval.getValue() * UnitConversions.KM_TO_M) : (float) (interval.getValue() * UnitConversions.MI_TO_M);
|
||||||
|
intervalStats.build(trackPoints, distanceInterval);
|
||||||
|
return intervalStats;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<IntervalStatistics> getIntervalStats(@Nullable Track.Id trackId, @Nullable IntervalOption interval) {
|
public void add(TrackPoint trackPoint) {
|
||||||
if (interval == null) {
|
synchronized (trackPoints) {
|
||||||
interval = IntervalOption.OPTION_1;
|
trackPoints.add(trackPoint);
|
||||||
}
|
}
|
||||||
if (trackId != null) {
|
|
||||||
loadIntervalStats(trackId, interval);
|
|
||||||
}
|
|
||||||
return intervalStats;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadIntervalStats(final Track.Id trackId, IntervalOption interval) {
|
public void clear() {
|
||||||
new Thread(() -> {
|
synchronized (trackPoints) {
|
||||||
Context context = getApplication().getApplicationContext();
|
trackPoints.clear();
|
||||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
}
|
||||||
List<TrackPoint> trackPointList = contentProviderUtils.getTrackPoints(trackId);
|
|
||||||
|
|
||||||
IntervalStatistics intervalStatistics = new IntervalStatistics();
|
|
||||||
float distanceInterval = PreferencesUtils.isMetricUnits(context) ? (float) (interval.getValue() * UnitConversions.KM_TO_M) : (float) (interval.getValue() * UnitConversions.MI_TO_M);
|
|
||||||
intervalStatistics.build(trackPointList, distanceInterval);
|
|
||||||
|
|
||||||
intervalStats.postValue(intervalStatistics);
|
|
||||||
}).start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,7 +55,7 @@ public class IntervalStatisticsModel extends AndroidViewModel {
|
|||||||
OPTION_20(20),
|
OPTION_20(20),
|
||||||
OPTION_50(50);
|
OPTION_50(50);
|
||||||
|
|
||||||
private int value;
|
private final int value;
|
||||||
|
|
||||||
IntervalOption(int value) {
|
IntervalOption(int value) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
|||||||