Compare commits

...

45 Commits

Author SHA1 Message Date
Román Ginés f9b8e46b32 Release: v4.0.0 2022-05-22 21:53:13 +02:00
Dennis Guse c79fc2f353 Change track title capitalization to sentences 2022-05-22 21:12:38 +02:00
gerberfg e283e993bb Change track title capitalization to sentences 2022-05-22 21:09:57 +02:00
Egor Ermakov 3f1e74e71e Translated using Weblate (Russian)
Currently translated at 100.0% (502 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2022-05-21 21:04:10 +02:00
mondstern 300453e3e6 Translated using Weblate (Dutch)
Currently translated at 100.0% (502 of 502 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nl/
2022-05-21 21:04:10 +02:00
Dennis Guse ac8eea377e Bugfix: resume functionality was not starting recording.
Introduced in 9d64989ef1
2022-05-21 09:16:43 +02:00
dependabot[bot] cc44e8e9bf Bump constraintlayout from 2.1.3 to 2.1.4
Bumps [constraintlayout](https://github.com/androidx/constraintlayout) from 2.1.3 to 2.1.4.
- [Release notes](https://github.com/androidx/constraintlayout/releases)
- [Commits](https://github.com/androidx/constraintlayout/commits)

---
updated-dependencies:
- dependency-name: androidx.constraintlayout:constraintlayout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-20 07:05:15 +02:00
mm4c 3860472e6b Translated using Weblate (Dutch)
Currently translated at 87.2% (75 of 86 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/nl/
2022-05-19 19:30:48 +02:00
metezd 69029486b6 Translated using Weblate (Turkish)
Currently translated at 98.9% (494 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/tr/
2022-05-19 19:30:48 +02:00
Adriel Sand b407f86d5e Translated using Weblate (Russian)
Currently translated at 100.0% (499 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/ru/
2022-05-19 19:30:48 +02:00
Adriel Sand 731adc7e31 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (499 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2022-05-19 19:30:48 +02:00
mondstern 858149e718 Translated using Weblate (Dutch)
Currently translated at 100.0% (499 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nl/
2022-05-19 19:30:48 +02:00
mondstern 34e796f73c Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (499 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2022-05-19 19:30:47 +02:00
Dennis Guse 3ac71361b8 Launcher icon: provide a themed icon (Android 13). 2022-05-19 19:30:43 +02:00
Dennis Guse e409b315b4 Refactor permission requests.
Fixes #1224.
2022-05-19 19:30:20 +02:00
Dennis Guse 24eb768707 Export: filename template now distinguishes between {date} and {time}. 2022-05-19 19:28:15 +02:00
Peter Storch 17ad13d559 Add ExceptionHandler and ShowErrorActivity 2022-05-19 06:12:58 +02:00
Dennis Guse 3b71212905 Public API: respect post-workout export.
Fixes #1225.
2022-05-15 20:19:59 +02:00
Dennis Guse 16a29c5e31 Bugfix: for a recording Track TrackListActivity needs to open TrackRecordingActivity.
Introduced in 9d64989ef1

Fixes #1235.
2022-05-15 08:00:35 +02:00
Dennis Guse e421164282 Bugfix: fileformat template was not loaded (NPE).
Fixes #1232.
2022-05-13 19:28:43 +02:00
Pixtxa de01157964 Changed second t.getLatitude() to t.getLongitude()
I thought it was excel until I realized, the data is also broken in newly generated csv files
2022-05-12 22:30:01 +02:00
Dennis Guse 8ef51ec318 Bugfix: unitSystem not loaded correctly.
Introduced in a5b44175a3

Fixes #1228.
2022-05-12 07:44:34 +02:00
Dennis Guse d2139d70ad Bugfix: sharing track added file suffix twice.
Introduced in 0d3cf4ce0d.
2022-05-11 22:22:45 +02:00
metezd d1a37366be Translated using Weblate (Turkish)
Currently translated at 79.0% (68 of 86 strings)

Translation: OpenTracks/App store metadata files
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/app-store-metadata-files/tr/
2022-05-11 20:56:56 +02:00
metezd 52d7c8a6cb Translated using Weblate (Turkish)
Currently translated at 79.5% (397 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/tr/
2022-05-11 20:56:56 +02:00
metezd c62e56e5a3 Translated using Weblate (Turkish)
Currently translated at 78.1% (390 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/tr/
2022-05-11 20:56:56 +02:00
Xosé M 6b89092adf Translated using Weblate (Galician)
Currently translated at 100.0% (499 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2022-05-11 20:56:56 +02:00
Mario 118a56b8d6 Translated using Weblate (Spanish)
Currently translated at 100.0% (499 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es/
2022-05-11 20:56:56 +02:00
Mario 7fb993014c Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (499 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es_419/
2022-05-11 20:56:56 +02:00
Dennis Guse 09f72af66c Deleted translation using Weblate (Sinhala) 2022-05-11 20:56:56 +02:00
Dennis Guse 815f111ca6 TrackRecordingService: use main looper explicitly. 2022-05-11 20:56:52 +02:00
Dennis Guse 40d2bf0c22 Bluetooth: use TrackRecording handler for callback.
Otherwise the thread is not defined and a SecurityException may be triggered when inserting data into the ContentProvider.
Sadly, below API26 this needs to be done manually.

Fixes #1165.
2022-05-11 20:56:52 +02:00
Dennis Guse a77b7a0626 Update Android build gradle plugin to 7.2.0 2022-05-11 19:59:02 +02:00
Dennis Guse 1a00b4b779 UI tests slightly more robust. 2022-05-11 19:59:02 +02:00
Dennis Guse 6c5c47240c Move to androidx.constraintlayout. 2022-05-11 19:59:02 +02:00
Dennis Guse 3c32da970b Upgrade to gradlew 7.4.2 2022-05-09 22:35:50 +02:00
Román Ginés 9cff62c5ba New screenshots after UI refactoring. 2022-05-09 20:38:25 +02:00
dependabot[bot] 424f93b4da Bump material from 1.5.0 to 1.6.0
Bumps [material](https://github.com/material-components/material-components-android) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/material-components/material-components-android/releases)
- [Commits](https://github.com/material-components/material-components-android/compare/1.5.0...1.6.0)

---
updated-dependencies:
- dependency-name: com.google.android.material:material
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-08 20:53:53 +02:00
Dennis Guse 3a23a412c8 Cleanup: use explicit preference identifiers for TrackFileFormat. 2022-05-08 20:49:24 +02:00
Dennis Guse 9d64989ef1 Public API: implemented STOP properly.
Fixes #1103.
2022-05-08 20:29:13 +02:00
Román Ginés da62d26409 UI refactoring with a Bottom App Bar.
Only one button to play/stop.
Stop Activity to edit track just recorded, resuming or finishing.
Logo color to FF6D00.
Fixes #1078 #437 #575 #1094.
2022-05-08 20:06:24 +02:00
Dennis Guse a350077e5c Merge remote-tracking branch 'weblate/main' 2022-05-08 11:22:26 +02:00
Dennis Guse 36b6cd3c98 Release: v3.28.0 2022-05-08 11:16:19 +02:00
metezd 203b457ad0 Translated using Weblate (Turkish)
Currently translated at 60.5% (302 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/tr/
2022-05-08 10:59:46 +02:00
metezd 0873b3bd12 Translated using Weblate (Turkish)
Currently translated at 57.1% (285 of 499 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/tr/
2022-05-08 10:59:46 +02:00
138 changed files with 22396 additions and 2563 deletions
+1
View File
@@ -130,6 +130,7 @@ The API can be invoked by sending an explicit Intent to start an activity.
`Class`:
* **Start a recording:** `de.dennisguse.opentracks.publicapi.StartRecording`
* **Start a recording:** `de.dennisguse.opentracks.publicapi.StopRecording`
For testing via adb: `adb shell am start -n "package/class"`
+1 -1
View File
@@ -5,7 +5,7 @@ set -e
# Folder where svg screenshots are expected.
SRC_DIR="screenshots-svg/"
# Folder where png files will be saved.
DST_DIR="fastlane/metadata/android/en-US/phoneScreenshots/"
DST_DIR="fastlane/metadata/android/en-US/images/phoneScreenshots/"
echo "Exporting svg files to png..."
+7 -7
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.3'
classpath 'com.android.tools.build:gradle:7.2.0'
}
}
@@ -51,7 +51,7 @@ android {
targetCompatibility JavaVersion.VERSION_11
}
compileSdkVersion 31
compileSdkVersion 32
buildFeatures {
viewBinding true
@@ -59,13 +59,13 @@ android {
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 4816
versionName "v3.27.0"
versionCode 4874
versionName "v4.0.0"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdkVersion 21
targetSdkVersion 31
targetSdkVersion 32
testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
@@ -122,8 +122,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'com.android.support.constraint:constraint-layout:2.0.4'
implementation 'com.google.android.material:material:1.6.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
androidTestImplementation 'androidx.test:core:1.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+114 -32
View File
@@ -1,37 +1,119 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
enable-background="new 0 0 95.334 100"
height="100"
id="Ebene_1"
version="1.1"
viewBox="0 0 100 100"
width="100"
x="0px"
xmlns="http://www.w3.org/2000/svg"
y="0px"
xml:space="preserve"><metadata
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
enable-background="new 0 0 95.334 100"
height="100"
id="Ebene_1"
version="1.1"
viewBox="0 0 100 100"
width="100"
x="0px"
y="0px"
xml:space="preserve"
sodipodi:docname="LOGO.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1011"
id="namedview10"
showgrid="false"
inkscape:zoom="7.94"
inkscape:cx="50"
inkscape:cy="50"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Ebene_1" /><metadata
id="metadata16"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata>
<defs id="defs14" />
<g id="g9"
transform="translate(-1.0593197,1.9067775)">
<linearGradient gradientUnits="userSpaceOnUse"
id="SVGID_1_"
x1="28.7554"
x2="82.789497"
y1="81.535202"
y2="9.2979002">
<stop style="stop-color:#FBB22F"
id="stop2"
offset="0" />
<stop style="stop-color:#F58820"
id="stop4"
offset="1" />
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata>
<defs
id="defs14"><linearGradient
inkscape:collect="always"
id="linearGradient862"><stop
style="stop-color:#ff6d00;stop-opacity:1;"
offset="0"
id="stop858" /><stop
style="stop-color:#ff6d00;stop-opacity:0;"
offset="1"
id="stop860" /></linearGradient><linearGradient
gradientUnits="userSpaceOnUse"
id="SVGID_1_"
x1="28.7554"
x2="82.789497"
y1="81.535202"
y2="9.2979002"
style="fill-opacity:1;fill:url(#linearGradient849)">
<stop
style="stop-color:#FBB22F;fill-opacity:1;fill:url(#linearGradient849)"
id="stop853"
offset="0" />
<stop
style="stop-color:#F58820;fill-opacity:1;fill:url(#linearGradient849)"
id="stop855"
offset="1" />
</linearGradient><linearGradient
inkscape:collect="always"
id="linearGradient847"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop843" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop845" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient847"
id="linearGradient849"
x1="83.161415"
y1="8.9067831"
x2="10.194695"
y2="92.068085"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient862"
id="linearGradient866"
gradientUnits="userSpaceOnUse"
x1="62.43222"
y1="35.349697"
x2="9.7942638"
y2="93.02298" /></defs>
<g
id="g9"
transform="translate(-1.0593197,1.9067775)"
style="fill-opacity:1;fill:url(#linearGradient849)">
<linearGradient
gradientUnits="userSpaceOnUse"
id="SVGID_1_-3"
x1="28.7554"
x2="82.789497"
y1="81.535202"
y2="9.2979002"
style="fill-opacity:1;fill:url(#linearGradient849)">
<stop
style="stop-color:#FBB22F;fill-opacity:1;fill:url(#linearGradient849)"
id="stop2"
offset="0" />
<stop
style="stop-color:#F58820;fill-opacity:1;fill:url(#linearGradient849)"
id="stop4"
offset="1" />
</linearGradient>
<path style="fill:url(#SVGID_1_)"
d="M 83.548,10.927 C 84.255,8.511 83.102,7.649 80.985,9.01 L 11.146,53.921 c -2.117,1.361 -1.795,2.628 0.714,2.815 L 40.865,58.9 c 2.51,0.188 5.291,2.267 6.18,4.621 l 10.282,27.208 c 0.89,2.354 2.195,2.304 2.902,-0.111 z"
id="path7" />
<path
style="fill:url(#linearGradient866);fill-opacity:1"
d="M 83.548,10.927 C 84.255,8.511 83.102,7.649 80.985,9.01 L 11.146,53.921 c -2.117,1.361 -1.795,2.628 0.714,2.815 L 40.865,58.9 c 2.51,0.188 5.291,2.267 6.18,4.621 l 10.282,27.208 c 0.89,2.354 2.195,2.304 2.902,-0.111 z"
id="path7" />
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

+37
View File
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
enable-background="new 0 0 95.334 100"
height="100"
id="Ebene_1"
version="1.1"
viewBox="0 0 100 100"
width="100"
x="0px"
xmlns="http://www.w3.org/2000/svg"
y="0px"
xml:space="preserve"><metadata
id="metadata16"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata>
<defs id="defs14" />
<g id="g9"
transform="translate(-1.0593197,1.9067775)">
<linearGradient gradientUnits="userSpaceOnUse"
id="SVGID_1_"
x1="28.7554"
x2="82.789497"
y1="81.535202"
y2="9.2979002">
<stop style="stop-color:#FBB22F"
id="stop2"
offset="0" />
<stop style="stop-color:#F58820"
id="stop4"
offset="1" />
</linearGradient>
<path style="fill:url(#SVGID_1_)"
d="M 83.548,10.927 C 84.255,8.511 83.102,7.649 80.985,9.01 L 11.146,53.921 c -2.117,1.361 -1.795,2.628 0.714,2.815 L 40.865,58.9 c 2.51,0.188 5.291,2.267 6.18,4.621 l 10.282,27.208 c 0.89,2.354 2.195,2.304 2.902,-0.111 z"
id="path7" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+67
View File
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="125"
id="svg2"
version="1.1"
viewBox="0 0 125 125"
width="125"
sodipodi:docname="ic_button_done.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1011"
id="namedview9"
showgrid="false"
inkscape:zoom="2.2457711"
inkscape:cx="104.26334"
inkscape:cy="71.321036"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<defs
id="defs6" />
<circle
style="fill:none;fill-opacity:1;stroke:#ff6d00;stroke-width:5.03185;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="62.5"
cy="62.5"
id="path12"
r="59.984074" />
<metadata
id="metadata8">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<circle
style="fill:#ff6d00;fill-opacity:1;stroke:none;stroke-width:6.91728;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="62.5"
cy="62.5"
id="path835"
r="50" />
<path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:6.2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 38.620483,63.865643 52.899055,78.144215 86.379516,44.663754"
id="path1000" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+79
View File
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="125"
id="svg2"
version="1.1"
viewBox="0 0 125 125"
width="125"
sodipodi:docname="ic_button_resume.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1011"
id="namedview9"
showgrid="false"
inkscape:zoom="3.176"
inkscape:cx="85.245821"
inkscape:cy="70.9649"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<defs
id="defs6" />
<circle
style="fill:none;fill-opacity:1;stroke:#ff6d00;stroke-width:5.03185;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="62.5"
cy="62.5"
id="path12"
r="59.984074" />
<circle
style="fill:#ff6d00;fill-opacity:1;stroke:none;stroke-width:6.91728;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="62.5"
cy="62.5"
id="path835"
r="50" />
<metadata
id="metadata8">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:title></dc:title>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<path
sodipodi:type="star"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:6.67087;stroke-linecap:round;stroke-opacity:1"
id="path837"
sodipodi:sides="3"
sodipodi:cx="61.453945"
sodipodi:cy="62.5"
sodipodi:r1="26.146271"
sodipodi:r2="13.073136"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="M 87.600216,62.5 67.990513,73.821668 48.38081,85.143335 l -10e-7,-22.643336 10e-7,-22.643334 19.609704,11.321667 z"
inkscape:transform-center-x="-6.5365678" />
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -4,3 +4,4 @@ Changes:
- Configurable filename for export/sharing (default: {starttime}_{name}.{suffix})
- Vertical GPS accuracy is now stored and can be exported/imported
(API level >= 26).
@@ -0,0 +1,24 @@
v4.0.0: OpenTracks
Changes:
- UI refactoring with a Bottom App Bar.
- Public API: implemented STOP properly.
- Cleanup: use explicit preference identifiers for TrackFileFormat.
- Improvements on BLE handler.
- Add exception handler and show error activity so users can send reports about bugs.
- Android 13: launcher icon, provide a themed icon.
- Change track title capitalization to sentences.
- Translations updated.
Bugfix:
- Sharing track added file suffix twice.
- Unit system not loaded correctly.
- File format template was not loaded (NPE).
- Public API: respect post-workout export.
Developer:
- Move to androix.constraintlayout.
- UI tests slightly more robust.
- Refactor permission request.
- Upgrade to gradlew 7.4.2
- Bump material from 1.5.0 to 1.6.0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 KiB

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 812 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 630 KiB

After

Width:  |  Height:  |  Size: 695 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 KiB

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 KiB

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 798 KiB

After

Width:  |  Height:  |  Size: 872 KiB

@@ -0,0 +1,16 @@
v3.18.6: OpenTracks
Wijzigingen:
- "Toon op kaart"-functionaliteit via KML in plaats van KMZ
- Spraakaankondigingen bevatten geen app naam
- Tijdens opname: hoogtewinst/-verlies wordt elke seconde bijgewerkt
- Winkel beschrijvingen vertaald
Bufixes:
- Ongeldige vertalingen van stemboodschappen opgelost
Ontwikkelaar:
- Android12-toestemmingen (BLUETOOTH_SCAN)
- Android12-toestemmingen (ACCESS_COARSE_LOCATION)
- Assertiviteit voor TrackPoints (testen)
- Ongebruikte vertalingen verwijderd
@@ -0,0 +1,16 @@
v3.19.0: OpenTracks
Wijzigingen:
- Ondersteuning voor Running Speed en Cadence (RCS) sensoren
- Bij het delen van een track wordt een afbeelding gemaakt
- Niet trillen bij starten van opnemen
- Bij opnemen: UI toont recentst ontvangen hoogte, snelheid en richting (ook al is deze niet opgeslagen)
Bugfix:
- Berekening van totale tijd mag geen pauze-tijd bevatten (vereist opnieuw importeren)
- (opnieuw) Gebruik aangepaste overzichten indien aanwezig
Ontwikkelaar:
- Upgrade naar SDK31 en AGP7.0.0 (vereist JDK11)
- TrackRecordingService: sensorfusie (TrackPointCreator) en TrackPoint-opslag (TrackRecordingManager) geëxtraheerd
- ExportImportTest is nu deterministisch/constant
@@ -0,0 +1,22 @@
Gizliliğinize saygı duyan bir spor takipçisi.
Spor ve açık hava etkinlikleri mutluluk, beceri ve güven verir.
Antrenmanınızı takip ederek sağlığınıza değer verin.
Koşarken veya yürürken kaydeder ve size bisiklet sürmek için daha büyük ekranlı bir bisiklet bilgisayarı verir.
Yol boyunca ilginç yerleri resimlerle işaretleyin.
Analiz için kaydedilen istatistikleri çok ayrıntılı tutun.
Yalnızca başkalarının sahip olmasını istediğiniz verileri paylaşın.
* Sesli duyurular.
* Bluetooth LE sensörlerini destekler: kalp atış hızı, hız ve mesafe (bisiklet), tempo (bisiklet) ve güç ölçer (bisiklet).
* İrtifa kazancı ve kaybı: barometrik sensör aracılığıyla.
* EGM2008'de gösterilen rakım (ortalama deniz seviyesinin üzerinde); WGS84 olarak dışa aktarılır.
* Verileri KMZ (fotoğraflar dahil), KML veya GPX olarak parça olarak dışa aktarın.
* İnternet erişimi veya ekstra izin yok.
* Sistem ayarlarına uygun olarak koyu ve açık tema.
* Reklamsız.
Libre yazılımı / Özgür Yazılım / Açık Kaynak
yani kaynak kodunu kullanabilir, inceleyebilir, değiştirebilir ve paylaşabilirsiniz.
Lisanslı Apache 2.0
@@ -0,0 +1 @@
Gizliliğinize saygı duyan bir spor takipçisi.
-1
View File
@@ -1,2 +1 @@
android.enableJetifier=true
android.useAndroidX=true
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 116 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 862 KiB

After

Width:  |  Height:  |  Size: 474 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 240 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 298 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 665 KiB

After

Width:  |  Height:  |  Size: 565 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 237 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 661 KiB

After

Width:  |  Height:  |  Size: 588 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1001 KiB

After

Width:  |  Height:  |  Size: 1012 KiB

@@ -78,17 +78,16 @@ public class EspressoDeleteTrackTest {
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_list_fab_action));
trackControllerRecordButton.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
// wait; stay recording
trackControllerRecordButton.perform(waitFor(5000));
trackControllerStopButton.perform(waitFor(5000));
// stop;
trackControllerStopButton.perform(veryLongTouch(1600));
@@ -108,7 +107,7 @@ public class EspressoDeleteTrackTest {
allOf(withContentDescription("More options"),
childAtPosition(
childAtPosition(
withId(R.id.toolbar),
withId(R.id.bottom_app_bar),
2),
2),
isDisplayed()));
@@ -5,18 +5,14 @@ import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.is;
import static de.dennisguse.opentracks.util.EspressoUtils.childAtPosition;
import static de.dennisguse.opentracks.util.EspressoUtils.veryLongTouch;
import static de.dennisguse.opentracks.util.EspressoUtils.waitFor;
import androidx.test.espresso.ViewInteraction;
@@ -25,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import org.hamcrest.core.IsInstanceOf;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -50,16 +45,15 @@ public class EspressoEditTrackRecordingTest {
public void espressoEditTrackRecordingTest() {
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
trackControllerRecordButton.perform(click());
ViewInteraction fabRecordButton = onView(withId(R.id.track_list_fab_action));
fabRecordButton.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
// wait; stay recording
trackControllerRecordButton.perform(waitFor(15000));
trackControllerStopButton.perform(waitFor(4000));
// open menu
openContextualActionModeOverflowMenu();
@@ -68,58 +62,24 @@ public class EspressoEditTrackRecordingTest {
onView(withText(R.string.menu_edit)).perform(click());
// change name for "New Name"
ViewInteraction textInputEditText = onView(
allOf(withId(R.id.track_edit_name),
childAtPosition(
childAtPosition(
withClassName(is("com.google.android.material.textfield.TextInputLayout")),
0),
1)));
ViewInteraction textInputEditText = onView(withId(R.id.track_edit_name));
textInputEditText.perform(scrollTo(), replaceText("New Name"));
ViewInteraction textInputEditText2 = onView(
allOf(withId(R.id.track_edit_name), withText("New Name"),
childAtPosition(
childAtPosition(
withClassName(is("com.google.android.material.textfield.TextInputLayout")),
0),
1),
isDisplayed()));
ViewInteraction textInputEditText2 = onView(allOf(withId(R.id.track_edit_name), withText("New Name"), isDisplayed()));
textInputEditText2.perform(closeSoftKeyboard());
// save edition
ViewInteraction appCompatButton = onView(
allOf(withId(R.id.track_edit_save),
childAtPosition(
childAtPosition(
withClassName(is("android.widget.LinearLayout")),
3),
1),
isDisplayed()));
ViewInteraction appCompatButton = onView(withId(R.id.track_edit_save));
appCompatButton.perform(click());
//pause
trackControllerRecordButton.perform(veryLongTouch(1600));
// wait; stay paused
trackControllerRecordButton.perform(waitFor(1000));
// resume
trackControllerRecordButton.perform(click());
// wait; stay recording
trackControllerRecordButton.perform(waitFor(1000));
// stop;
trackControllerStopButton.perform(veryLongTouch(1600));
trackControllerStopButton.perform(longClick());
// check track's name is "New Name"
ViewInteraction textView = onView(
allOf(withId(R.id.stats_name_value),
withParent(withParent(IsInstanceOf.instanceOf(android.widget.ScrollView.class))),
isDisplayed()));
textView.check(matches(withText("New Name")));
// it's on track stopped activity and there are two buttons
ViewInteraction resumeButton = onView(withId(R.id.resume_button));
resumeButton.check(matches(isDisplayed()));
ViewInteraction finishButton = onView(withId(R.id.finish_button));
finishButton.check(matches(isDisplayed()));
}
}
}
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks;
import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
@@ -39,34 +40,30 @@ public class EspressoUITest {
@LargeTest
@Test
public void record_pause_resume_stop() {
public void record_stop_resume_stop_finish() {
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_list_fab_action));
trackControllerRecordButton.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
// TrackRecordingActivity: wait to record some time and then stop
onView(withId(R.id.track_recording_fab_action))
.perform(waitFor(5000))
.perform(longClick());
// wait; stay recording
trackControllerRecordButton.perform(waitFor(5000));
// TrackStoppedActivity: resume
onView(withId(R.id.resume_button))
.perform(click());
//pause
trackControllerRecordButton.perform(veryLongTouch(1600));
// TrackRecordingActivity: wait and then stop
onView(withId(R.id.track_recording_fab_action))
.perform(waitFor(5000))
.perform(longClick());
// wait; stay paused
trackControllerRecordButton.perform(waitFor(1000));
// resume
trackControllerRecordButton.perform(click());
// wait; stay recording
trackControllerRecordButton.perform(waitFor(1000));
// stop;
trackControllerStopButton.perform(veryLongTouch(1600));
// TrackStoppedActivity
onView(withId(R.id.finish_button))
.perform(click());
}
}
@@ -75,13 +72,13 @@ public class EspressoUITest {
public void record_move_through_tabs() {
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.controller_record));
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_list_fab_action));
trackControllerRecordButton.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
tabLayout.perform(selectTabAtIndex(1));
tabLayout.perform(waitFor(1000));
@@ -29,7 +29,7 @@ public class ShareContentProviderTest {
trackIds.add(new Track.Id(3));
trackIds.add(new Track.Id(5));
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "TrackName", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "filename.suffix", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
assertEquals(trackIds, ShareContentProvider.parseURI(shareURIandMIME.first));
}
@@ -40,6 +40,6 @@ public class ShareContentProviderTest {
trackIds.add(new Track.Id(1));
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "../../&1=1", TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA);
assertEquals(Uri.parse("content://de.dennisguse.opentracks.debug.content/tracks/kml_with_trackdetail_and_sensordata/1/..%2F..%2F%261%3D1.kml"), shareURIandMIME.first);
assertEquals(Uri.parse("content://de.dennisguse.opentracks.debug.content/tracks/KML_WITH_TRACKDETAIL_AND_SENSORDATA/1/..%2F..%2F%261%3D1"), shareURIandMIME.first);
}
}
@@ -28,10 +28,11 @@ public class TrackFilenameGeneratorTest {
@Parameterized.Parameters
public static Collection<String[]> data() {
return Arrays.asList(new String[][]{
{"{uuid}_{name}", "0000fee0_Best Track.gpx" },
{"{name}_{uuid}", "Best Track_0000fee0.gpx" },
{"{time}_{name}", "2020-02-02T02_02_02Z_Best Track.gpx" },
{ApplicationProvider.getApplicationContext().getString(R.string.export_filename_format_default), "2020-02-02T02_02_02Z_Best Track.gpx" },
{"{uuid}_{name}", "0000fee0_Best Track.gpx"},
{"{name}_{uuid}", "Best Track_0000fee0.gpx"},
{"{time}_{name}", "02_02_02_Best Track.gpx"},
{"{date}_{name}", "2020-02-02_Best Track.gpx"},
{ApplicationProvider.getApplicationContext().getString(R.string.export_filename_format_default), "2020-02-02_02_02_02_Best Track.gpx"},
});
}
@@ -162,7 +162,7 @@ public class ExportImportTest {
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
trackPointCreator.setClock("2020-02-02T02:02:06Z");
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, trackPointCreator));
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
service.pauseCurrentTrack();
trackPointCreator.setClock("2020-02-02T02:02:20Z");
@@ -175,7 +175,7 @@ public class ExportImportTest {
sendLocation(trackPointCreator, "2020-02-02T02:02:23Z", 3, 16.001, 10, 27, 15, 10, 0);
trackPointCreator.setClock("2020-02-02T02:02:24Z");
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, trackPointCreator));
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator));
service.endCurrentTrack();
Track track = contentProviderUtils.getTrack(trackId);
@@ -492,7 +492,7 @@ public class ExportImportTest {
}
private void mockBLESensorData(TrackPointCreator trackPointCreator, Float speed, Distance distance, float heartRate, float cadence, Float power) {
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, trackPointCreator) {
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, trackPointCreator) {
@Override
public SensorDataSet fill(@NonNull TrackPoint trackPoint) {
SensorDataSet sensorDataSet = new SensorDataSet();
@@ -27,7 +27,7 @@ public class TrackRecordingServiceTestUtils {
TrackRecordingService service = ((TrackRecordingService.Binder) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)))
.getService();
service.getTrackPointCreator().setRemoteSensorManager(new BluetoothRemoteSensorManager(context, service.getTrackPointCreator()));
service.getTrackPointCreator().setRemoteSensorManager(new BluetoothRemoteSensorManager(context, null, service.getTrackPointCreator()));
service.getTrackPointCreator().setClock(Clock.systemUTC());
service.endCurrentTrack();
}
@@ -32,7 +32,7 @@ public class TrackPointCreatorTest {
@Before
public void setUp() {
subject = new TrackPointCreator(locationHandler, server);
subject.start(context);
subject.start(context, null);
}
@After
@@ -58,7 +58,7 @@ public class PreferencesUtilsTest {
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat();
// then
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat);
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES, trackFileFormat);
}
@Test
@@ -73,7 +73,7 @@ public class PreferencesUtilsTest {
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat();
// then
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat);
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES, trackFileFormat);
}
@Test
@@ -59,13 +59,13 @@ public class EspressoAggregatedFilterTest {
@Test
public void espressoAggregatedFilterTest() {
// open AggregatedStatisticsActivity through toolbar's menu item
onView(ViewMatchers.withId(R.id.toolbar)).check(matches(isDisplayed()));
onView(withId(R.id.toolbar)).check(matches(hasDescendant(withId(R.id.track_list_aggregated_stats))));
onView(ViewMatchers.withId(R.id.bottom_app_bar)).check(matches(isDisplayed()));
onView(withId(R.id.bottom_app_bar)).check(matches(hasDescendant(withId(R.id.track_list_aggregated_stats))));
onView(withId(R.id.track_list_aggregated_stats)).perform(click());
// open FilterDialogFragment through toolbar's menu item
onView(withId(R.id.toolbar)).check(matches(isDisplayed()));
onView(withId(R.id.toolbar)).check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter))));
onView(withId(R.id.bottom_app_bar)).check(matches(isDisplayed()));
onView(withId(R.id.bottom_app_bar)).check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter))));
onView(withId(R.id.aggregated_statistics_filter)).perform(click());
// check there's a checkbox with CATEGORY text
@@ -41,7 +41,7 @@ public class EspressoCustomLayoutTest {
@Test
public void customLayoutTest() {
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(ViewMatchers.withId(R.id.controller_record));
ViewInteraction trackControllerRecordButton = onView(ViewMatchers.withId(R.id.track_list_fab_action));
trackControllerRecordButton.perform(click());
// Get custom layout preferences and check all data fields are showed.
@@ -50,7 +50,7 @@ public class EspressoCustomLayoutTest {
onView(withId(R.id.stats_recycler_view)).check(new RecyclerViewItemCountAssertion((int) layout.getFields().stream().filter(DataField::isVisible).count()));
// stop recording
ViewInteraction trackControllerStopButton = onView(withId(R.id.controller_stop));
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_recording_fab_action));
trackControllerStopButton.perform(veryLongTouch(1600));
}
+17 -8
View File
@@ -77,14 +77,14 @@ limitations under the License.
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- <activity-->
<!-- android:name=".publicapi.StopRecording"-->
<!-- android:exported="true"-->
<!-- android:theme="@style/SplashTheme">-->
<!-- <intent-filter>-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- </intent-filter>-->
<!-- </activity>-->
<activity
android:name=".publicapi.StopRecording"
android:exported="true"
android:theme="@style/SplashTheme">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".AboutActivity" />
@@ -324,6 +324,7 @@ limitations under the License.
</activity>
<activity android:name=".TrackRecordedActivity" />
<activity android:name=".TrackStoppedActivity" />
<activity
android:name=".TrackRecordingActivity"
@@ -363,6 +364,14 @@ limitations under the License.
</intent-filter>
</activity>
<activity
android:name=".ShowErrorActivity"
android:excludeFromRecents="true"
android:exported="false"
android:finishOnTaskLaunch="true"
android:launchMode="singleInstance"
android:process=":crash"/>
<provider
android:name=".data.ShareContentProvider"
android:authorities="${applicationId}.content"
@@ -24,6 +24,8 @@ public class AboutActivity extends AbstractActivity {
viewBinding.aboutTextVersionCode.setText(getString(R.string.about_version_code, SystemUtils.getAppVersionCode(this)));
viewBinding.aboutAppUrl.setText(getString(R.string.about_url, getString(R.string.app_web_url)));
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
ViewUtils.makeClickableLinks(findViewById(android.R.id.content));
}
@@ -19,9 +19,7 @@ package de.dennisguse.opentracks;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import de.dennisguse.opentracks.services.announcement.VoiceAnnouncement;
@@ -38,9 +36,6 @@ public abstract class AbstractActivity extends AppCompatActivity {
setVolumeControlStream(VoiceAnnouncement.AUDIO_STREAM);
setContentView(getRootView());
Toolbar toolbar = findViewById(R.id.toolbar);
setupActionBarBack(toolbar);
}
@Override
@@ -50,12 +45,4 @@ public abstract class AbstractActivity extends AppCompatActivity {
}
protected abstract View getRootView();
protected void setupActionBarBack(@Nullable Toolbar toolbar) {
setSupportActionBar(toolbar);
if (toolbar != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
}
}
@@ -1,264 +0,0 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.TransitionDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import de.dennisguse.opentracks.databinding.ControllerFragmentBinding;
import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.services.RecordingStatus;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.ui.util.ActivityUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Track controller for record, pause, resume, and stop.
*
* @author Jimmy Shih
*/
//TODO Move buttons into separate components
public class ControllerFragment extends Fragment implements View.OnTouchListener {
private static final String TAG = ControllerFragment.class.getSimpleName();
private Handler handlerUpdateTotalTime;
private Runnable buttonDelay;
private int buttonDelayDuration;
private ControllerFragmentBinding viewBinding;
private TransitionDrawable transition;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private RecordingStatus recordingStatus;
private RecordingData recordingData;
private final TrackRecordingServiceConnection.Callback bindChangedCallback = service -> {
service.getRecordingStatusObservable()
.observe(ControllerFragment.this, this::onRecordingStatusChanged);
service.getRecordingDataObservable()
.observe(ControllerFragment.this, this::onTotalTimeChanged);
};
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
buttonDelayDuration = getResources().getInteger(R.integer.buttonDelayMillis);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
handlerUpdateTotalTime = new Handler();
recordingStatus = TrackRecordingService.STATUS_DEFAULT;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
viewBinding = ControllerFragmentBinding.inflate(inflater, container, false);
viewBinding.controllerRecord.setOnTouchListener(this);
viewBinding.controllerRecord.setOnClickListener((view) -> {
if (buttonDelay != null || recordingStatus.isRecordingAndNotPaused()) {
return;
}
Callback callback = (Callback) getContext();
if (callback != null) {
callback.recordStart();
}
});
viewBinding.controllerStop.setOnTouchListener(this);
transition = (TransitionDrawable) viewBinding.controllerContainer.getBackground();
onRecordingStatusChanged(recordingStatus);
return viewBinding.getRoot();
}
@Override
public void onResume() {
super.onResume();
trackRecordingServiceConnection.startConnection(getContext());
}
@Override
public void onPause() {
super.onPause();
handlerUpdateTotalTime.removeCallbacksAndMessages(null);
}
@Override
public void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind(getContext());
}
@Override
public void onDestroyView() {
super.onDestroyView();
viewBinding = null;
transition = null;
}
@Override
public void onDestroy() {
super.onDestroy();
trackRecordingServiceConnection = null;
handlerUpdateTotalTime = null;
recordingStatus = null;
}
//TODO Move into a separate object
@SuppressLint("ClickableViewAccessibility") //We do accessibility manually.
@Override
public boolean onTouch(final View view, MotionEvent event) {
if (!isResumed()) {
Log.w(TAG, "The UI is already gone, no need to update anything; seen on Android 7.1.2");
return false;
}
if (event.getAction() == MotionEvent.ACTION_MOVE) {
return false;
}
if (buttonDelay != null && event.getAction() == MotionEvent.ACTION_UP) {
handlerUpdateTotalTime.removeCallbacks(buttonDelay);
buttonDelay = null;
view.setPressed(false);
transition.resetTransition();
showStatusSetDefaultText();
return false;
}
//Trigger button pressed animation
view.setPressed(true);
//To pause a recording
if (viewBinding.controllerRecord.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& recordingStatus.isRecordingAndNotPaused()) {
transition.startTransition(buttonDelayDuration);
ActivityUtils.vibrate(getContext(), 150);
viewBinding.controllerStatus.setText(R.string.hold_to_pause);
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
view.performClick();
Callback callback = (Callback) getContext();
if (callback != null) {
callback.recordPause();
}
transition.resetTransition();
ActivityUtils.vibrate(getContext(), 1000);
};
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
return true;
}
//To stop a recording
if (viewBinding.controllerStop.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& recordingStatus.isRecording()) {
transition.startTransition(buttonDelayDuration);
ActivityUtils.vibrate(getContext(), 150);
viewBinding.controllerStatus.setText(R.string.hold_to_stop);
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
Context context = getContext();
Callback callback = (Callback) context;
if (callback != null) {
callback.recordStop();
ActivityUtils.vibrate(context, 1000);
}
transition.resetTransition();
};
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
return true;
}
return false;
}
public void hide() {
viewBinding.controllerContainer.setVisibility(View.GONE);
}
public void show() {
viewBinding.controllerContainer.setVisibility(View.VISIBLE);
}
private void showStatusSetDefaultText() {
viewBinding.controllerStatus.setText(recordingStatus.isPaused() ? R.string.generic_paused : R.string.generic_recording);
}
public interface Callback {
void recordStart();
void recordPause();
void recordStop();
}
private void onRecordingStatusChanged(RecordingStatus status) {
recordingStatus = status;
viewBinding.controllerRecord.setImageResource(recordingStatus.isRecordingAndNotPaused() ? R.drawable.ic_button_pause : R.drawable.button_record);
viewBinding.controllerRecord.setContentDescription(getContext().getString(recordingStatus.isRecordingAndNotPaused() ? R.string.image_pause : R.string.image_record));
viewBinding.controllerStop.setEnabled(recordingStatus.isRecording());
viewBinding.controllerStatus.setVisibility(recordingStatus.isRecording() ? View.VISIBLE : View.INVISIBLE);
if (recordingStatus.isRecording()) {
viewBinding.controllerStatus.setTextColor(getContext().getResources().getColor(recordingStatus.isPaused() ? android.R.color.white : R.color.recording_text));
showStatusSetDefaultText();
}
viewBinding.controllerTotalTime.setVisibility(recordingStatus.isRecording() ? View.VISIBLE : View.INVISIBLE);
}
private void onTotalTimeChanged(RecordingData recordingData) {
this.recordingData = recordingData;
viewBinding.controllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(this.recordingData.getTrackStatistics().getTotalTime()));
}
}
@@ -10,14 +10,18 @@ import de.dennisguse.opentracks.ui.util.ViewUtils;
public class HelpActivity extends AbstractActivity {
private HelpBinding helpBinding;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setSupportActionBar(helpBinding.bottomAppBarLayout.bottomAppBar);
ViewUtils.makeClickableLinks(findViewById(android.R.id.content));
}
@Override
protected View getRootView() {
return HelpBinding.inflate(getLayoutInflater()).getRoot();
helpBinding = HelpBinding.inflate(getLayoutInflater());
return helpBinding.getRoot();
}
}
@@ -0,0 +1,88 @@
package de.dennisguse.opentracks;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import de.dennisguse.opentracks.databinding.ActivityShowErrorBinding;
import de.dennisguse.opentracks.databinding.TrackListBinding;
public class ShowErrorActivity extends AbstractActivity {
public static final String EXTRA_ERROR_TEXT = "error";
private ActivityShowErrorBinding viewBinding;
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewBinding.textViewError.setText(getIntent().getStringExtra(EXTRA_ERROR_TEXT));
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(createErrorTitle());
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
}
@Override
protected View getRootView() {
viewBinding = ActivityShowErrorBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
}
private String createErrorTitle() {
return String.format(getString(R.string.error_crash_title), getString(R.string.app_name));
}
private void reportBug() {
Uri uriUrl;
try {
uriUrl = Uri.parse(
String.format(
getString(R.string.report_issue_link),
URLEncoder.encode(viewBinding.textViewError.getText().toString(), StandardCharsets.UTF_8.toString())
)
);
} catch (final UnsupportedEncodingException ignored) {
// can't happen as UTF-8 is always available
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW, uriUrl);
startActivity(intent);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.show_error, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
if (item.getItemId() == R.id.error_share) {
onClickedShare();
return true;
} else if (item.getItemId() == R.id.error_report) {
reportBug();
return true;
}
return super.onOptionsItemSelected(item);
}
private void onClickedShare() {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_SUBJECT, createErrorTitle());
intent.putExtra(Intent.EXTRA_TEXT, viewBinding.textViewError.getText());
intent.setType("text/plain");
startActivity(intent);
}
}
@@ -1,10 +1,16 @@
package de.dennisguse.opentracks;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.os.StrictMode;
import android.util.Log;
import java.lang.reflect.Method;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.ExceptionHandler;
/**
* Code that is executed when the application starts.
@@ -37,6 +43,36 @@ public class Startup extends Application {
}
}
@Override
protected void attachBaseContext(final Context base) {
super.attachBaseContext(base);
// handle crashes only outside the crash reporter activity/process
if (!isCrashReportingProcess()) {
Thread.UncaughtExceptionHandler defaultPlatformHandler = Thread.getDefaultUncaughtExceptionHandler();
ExceptionHandler crashReporter = new ExceptionHandler(this, defaultPlatformHandler);
Thread.setDefaultUncaughtExceptionHandler(crashReporter);
}
}
private boolean isCrashReportingProcess() {
String processName = "";
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
// Using the same technique as Application.getProcessName() for older devices
// Using reflection since ActivityThread is an internal API
try {
@SuppressLint("PrivateApi")
Class<?> activityThread = Class.forName("android.app.ActivityThread");
@SuppressLint("DiscouragedPrivateApi") Method getProcessName = activityThread.getDeclaredMethod("currentProcessName");
processName = (String) getProcessName.invoke(null);
} catch (Exception ignored) {
}
} else {
processName = Application.getProcessName();
}
return processName != null && processName.endsWith(":crash");
}
/**
* Returns the name of the database used by SQLiteOpenHelper.
* See {@link android.database.sqlite.SQLiteOpenHelper} for details.
@@ -71,17 +71,17 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
return;
}
viewBinding.trackEditName.setText(track.getName());
viewBinding.fields.trackEditName.setText(track.getName());
viewBinding.trackEditActivityType.setText(track.getCategory());
viewBinding.fields.trackEditActivityType.setText(track.getCategory());
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
viewBinding.trackEditActivityType.setAdapter(adapter);
viewBinding.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.trackEditActivityType.getAdapter().getItem(position))));
viewBinding.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> {
viewBinding.fields.trackEditActivityType.setAdapter(adapter);
viewBinding.fields.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.fields.trackEditActivityType.getAdapter().getItem(position))));
viewBinding.fields.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackEditActivity.this, viewBinding.trackEditActivityType.getText().toString()));
TrackEditActivity.this, viewBinding.fields.trackEditActivityType.getText().toString()));
}
});
@@ -94,22 +94,23 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
}
setActivityTypeIcon(iconValue);
viewBinding.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.trackEditActivityType.getText().toString()));
viewBinding.fields.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.fields.trackEditActivityType.getText().toString()));
viewBinding.trackEditDescription.setText(track.getDescription());
viewBinding.fields.trackEditDescription.setText(track.getDescription());
viewBinding.trackEditSave.setOnClickListener(v -> {
TrackUtils.updateTrack(TrackEditActivity.this, track, viewBinding.trackEditName.getText().toString(),
viewBinding.trackEditActivityType.getText().toString(), viewBinding.trackEditDescription.getText().toString(),
TrackUtils.updateTrack(TrackEditActivity.this, track, viewBinding.fields.trackEditName.getText().toString(),
viewBinding.fields.trackEditActivityType.getText().toString(), viewBinding.fields.trackEditDescription.getText().toString(),
contentProviderUtils);
finish();
});
setTitle(R.string.menu_edit);
viewBinding.trackEditCancel.setOnClickListener(v -> finish());
viewBinding.trackEditCancel.setVisibility(View.VISIBLE);
}
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.menu_edit));
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
}
@Override
protected void onStart() {
@@ -137,12 +138,12 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
private void setActivityTypeIcon(String iconValue) {
this.iconValue = iconValue;
viewBinding.trackEditActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
viewBinding.fields.trackEditActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
}
@Override
public void onChooseActivityTypeDone(String value) {
setActivityTypeIcon(value);
viewBinding.trackEditActivityType.setText(getString(TrackIconUtils.getIconActivityType(value)));
viewBinding.fields.trackEditActivityType.setText(getString(TrackIconUtils.getIconActivityType(value)));
}
}
@@ -25,17 +25,19 @@ import android.database.Cursor;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Vibrator;
import android.provider.Settings;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.cursoradapter.widget.ResourceCursorAdapter;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
@@ -76,7 +78,7 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
*
* @author Leif Hendrik Wilden
*/
public class TrackListActivity extends AbstractTrackDeleteActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, ControllerFragment.Callback {
public class TrackListActivity extends AbstractTrackDeleteActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
private static final String TAG = TrackListActivity.class.getSimpleName();
@@ -101,6 +103,9 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
public void onPrepare(Menu menu, int[] positions, long[] trackIds, boolean showSelectAll) {
boolean isSingleSelection = trackIds.length == 1;
viewBinding.bottomAppBar.performHide(true);
viewBinding.trackListFabAction.setVisibility(View.INVISIBLE);
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll);
}
@@ -109,6 +114,12 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
public boolean onClick(int itemId, int[] positions, long[] trackIds) {
return handleContextItem(itemId, trackIds);
}
@Override
public void onDestroy() {
viewBinding.trackListFabAction.setVisibility(View.VISIBLE);
viewBinding.bottomAppBar.performShow(true);
}
};
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
@@ -127,7 +138,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem;
private final TrackRecordingServiceConnection.Callback bindChangedCallback = service -> {
private final TrackRecordingServiceConnection.Callback bindChangedCallback = (service, unused) -> {
service.getRecordingStatusObservable()
.observe(TrackListActivity.this, this::onRecordingStatusChanged);
@@ -155,16 +166,17 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> {
if (recordingStatus.isRecording() && trackId == recordingStatus.getTrackId().getId()) {
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackIdId) -> {
Track.Id trackId = new Track.Id(trackIdId);
if (recordingStatus.isRecording() && trackId.equals(recordingStatus.getTrackId())) {
// Is recording -> open record activity.
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, new Track.Id(trackId));
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
} else {
// Not recording -> open detail activity.
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordedActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, new Track.Id(trackId));
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
ActivityOptions activityOptions = ActivityOptions.makeSceneTransitionAnimation(
this,
new Pair<>(view.findViewById(R.id.list_item_icon), TrackRecordedActivity.VIEW_TRACK_ICON));
@@ -201,7 +213,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
String description = cursor.getString(descriptionIndex);
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording, recordingStatus.isPaused(),
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording,
iconId, R.string.image_track, name, totalTime, totalDistance, markerCount,
OffsetDateTime.ofInstant(Instant.ofEpochMilli(startTime), ZoneOffset.ofTotalSeconds(startTimeOffset)),
category, description, false);
@@ -210,13 +222,41 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
viewBinding.trackList.setAdapter(resourceCursorAdapter);
ActivityUtils.configureListViewContextualMenu(viewBinding.trackList, contextualActionModeCallback);
loadData(getIntent());
}
viewBinding.trackListFabAction.setOnClickListener((view) -> {
if (recordingStatus.isRecording()) {
Toast.makeText(TrackListActivity.this, getString(R.string.hold_to_stop), Toast.LENGTH_LONG).show();
return;
}
@Override
protected void setupActionBarBack(Toolbar toolbar) {
setSupportActionBar(toolbar);
toolbar.setNavigationIcon(R.drawable.ic_logo_color_24dp);
// Not Recording -> Recording
updateGpsMenuItem(false, true);
new TrackRecordingServiceConnection((service, connection) -> {
Track.Id trackId = service.startNewTrack();
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
newIntent.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
connection.unbind(this);
}).startAndBind(this);
});
viewBinding.trackListFabAction.setOnLongClickListener((view) -> {
if (!recordingStatus.isRecording()) {
return false;
}
// Recording -> Stop
((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).vibrate(1000);
updateGpsMenuItem(false, false);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
viewBinding.trackListFabAction.setImageResource(R.drawable.ic_baseline_record_24);
viewBinding.trackListFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.red_dark));
return true;
});
setSupportActionBar(viewBinding.bottomAppBar);
loadData(getIntent());
}
@Override
@@ -234,6 +274,9 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
// Update UI
this.invalidateOptionsMenu();
LoaderManager.getInstance(this).restartLoader(0, null, loaderCallbacks);
// Float button
setFloatButton();
}
@Override
@@ -263,6 +306,13 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
searchMenuItem = menu.findItem(R.id.track_list_search);
SearchView searchView = ActivityUtils.configureSearchWidget(this, searchMenuItem);
searchView.findViewById(R.id.search_edit_frame).setPadding(0, 0, 48, 0);
SearchView.SearchAutoComplete searchAutoComplete = searchView.findViewById(R.id.search_src_text);
searchAutoComplete.setHintTextColor(getResources().getColor(android.R.color.white));
searchAutoComplete.setTextColor(getResources().getColor(android.R.color.white));
searchView.setOnCloseListener(() -> {
searchView.clearFocus();
searchMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
@@ -515,38 +565,18 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
}
}
@Override
public void recordStart() {
if (recordingStatus.getTrackId() == null) {
// Not recording -> Recording
updateGpsMenuItem(false, true);
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
startActivity(newIntent);
} else if (recordingStatus.isPaused()) {
// Paused -> Resume
updateGpsMenuItem(false, true);
trackRecordingServiceConnection.resumeTrack();
}
}
@Override
public void recordPause() {
updateGpsMenuItem(false, true);
trackRecordingServiceConnection.pauseTrack();
}
@Override
public void recordStop() {
updateGpsMenuItem(false, false);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this);
}
public void onGpsStatusChanged(GpsStatusValue newStatus) {
gpsStatusValue = newStatus;
updateGpsMenuItem(true, recordingStatus.isRecording());
}
private void setFloatButton() {
viewBinding.trackListFabAction.setImageResource(recordingStatus.isRecording() ? R.drawable.ic_baseline_stop_24 : R.drawable.ic_baseline_record_24);
viewBinding.trackListFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, recordingStatus.isRecording() ? R.color.opentracks_secondary_color : R.color.red_dark));
}
private void onRecordingStatusChanged(RecordingStatus status) {
recordingStatus = status;
setFloatButton();
}
}
@@ -79,7 +79,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private final TrackRecordingServiceConnection.Callback bindCallback = service -> service.getRecordingStatusObservable()
private final TrackRecordingServiceConnection.Callback bindCallback = (service, unused) -> service.getRecordingStatusObservable()
.observe(TrackRecordedActivity.this, this::onRecordingStatusChanged);
@Override
@@ -102,6 +102,11 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindCallback);
Track track = contentProviderUtils.getTrack(trackId);
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(track != null ? track.getName() : "");
viewBinding.bottomAppBarLayout.bottomAppBar.replaceMenu(R.menu.track_detail);
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
postponeEnterTransition();
}
@@ -162,8 +167,6 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.track_detail_markers).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
menu.findItem(R.id.track_detail_resume_track).setVisible(!recordingStatus.isRecording());
Track track = contentProviderUtils.getTrack(trackId);
setTitle(track != null ? track.getName() : "");
return super.onPrepareOptionsMenu(menu);
}
@@ -200,11 +203,17 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
}
if (item.getItemId() == R.id.track_detail_resume_track) {
Intent newIntent = IntentUtils.newIntent(TrackRecordedActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
finish();
new TrackRecordingServiceConnection((service, connection) -> {
service.resumeTrack(trackId);
Intent newIntent = IntentUtils.newIntent(TrackRecordedActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
connection.unbind(this);
finish();
}).startAndBind(this);
return true;
}
@@ -246,7 +255,7 @@ public class TrackRecordedActivity extends AbstractTrackDeleteActivity implement
private void handleIntent(Intent intent) {
trackId = intent.getParcelableExtra(EXTRA_TRACK_ID);
if (trackId == null) {
Log.e(TAG, "TrackDetailActivity needs EXTRA_TRACK_ID.");
Log.e(TAG, TrackRecordedActivity.class.getSimpleName() + " needs EXTRA_TRACK_ID.");
finish();
}
}
@@ -1,11 +1,12 @@
package de.dennisguse.opentracks;
import android.Manifest;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Build;
import android.os.Bundle;
import android.os.Vibrator;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
@@ -13,9 +14,8 @@ import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
@@ -54,9 +54,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
//NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key.
//This mode of communication should be removed anyhow.
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackDataHubInterface, ControllerFragment.Callback {
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackDataHubInterface {
public static final String EXTRA_TRACK_ID = "track_id";
@@ -73,13 +71,11 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private TrackRecordingBinding viewBinding;
// Initialized from Intent; if a new track recording is started, a new TrackId will be provided by TrackRecordingService
@Deprecated
private Track.Id trackId;
private RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private final TrackRecordingServiceConnection.Callback bindChangedCallback = service -> {
private final TrackRecordingServiceConnection.Callback bindChangedCallback = (service, unused) -> {
service.getRecordingStatusObservable()
.observe(TrackRecordingActivity.this, this::onRecordingStatusChanged);
@@ -87,18 +83,12 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
.observe(TrackRecordingActivity.this, this::onGpsStatusChanged);
if (!service.isRecording()) {
if (trackId == null) {
// trackId isn't initialized -> leads a new recording.
trackId = service.startNewTrack();
} else {
// trackId is initialized -> resumes the track.
service.resumeTrack(trackId);
}
// A recording track is on.
trackDataHub.loadTrack(trackId);
trackDataHub.setRecordingStatus(recordingStatus);
finish();
return;
}
trackDataHub.loadTrack(trackId);
trackDataHub.setRecordingStatus(recordingStatus);
};
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
@@ -116,24 +106,18 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
runOnUiThread(TrackRecordingActivity.this::invalidateOptionsMenu); //TODO Should not be necessary
};
private MenuItem insertMarkerMenuItem;
private MenuItem markerListMenuItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
contentProviderUtils = new ContentProviderUtils(this);
trackId = null;
if (savedInstanceState != null) {
//Activity was recreated.
trackId = savedInstanceState.getParcelable(EXTRA_TRACK_ID);
} else {
// Resume a track
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
if (trackId != null && contentProviderUtils.getTrack(trackId) == null) {
Log.w(TAG, "TrackId does not exists; cannot continue the recording.");
finish();
}
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
if (trackId == null) {
throw new RuntimeException("TrackId is mandatory");
}
if (contentProviderUtils.getTrack(trackId) == null) {
Log.w(TAG, "TrackId does not exists.");
finish();
}
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
@@ -148,6 +132,26 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
requestGPSPermissions();
viewBinding.trackRecordingFabAction.setImageResource(R.drawable.ic_baseline_stop_24);
viewBinding.trackRecordingFabAction.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.opentracks_secondary_color));
viewBinding.trackRecordingFabAction.setBackgroundColor(getResources().getColor(R.color.opentracks_secondary_color));
viewBinding.trackRecordingFabAction.setOnLongClickListener((view) -> {
((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).vibrate(1000);
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this);
Intent newIntent = IntentUtils.newIntent(TrackRecordingActivity.this, TrackStoppedActivity.class)
.putExtra(TrackStoppedActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
finish();
return true;
});
viewBinding.trackRecordingFabAction.setOnClickListener((view) -> {
Toast.makeText(TrackRecordingActivity.this, getString(R.string.hold_to_stop), Toast.LENGTH_LONG).show();
});
viewBinding.bottomAppBar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24);
setSupportActionBar(viewBinding.bottomAppBar);
}
@Override
@@ -220,7 +224,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(CURRENT_TAB_TAG_KEY, viewBinding.trackDetailActivityViewPager.getCurrentItem());
outState.putParcelable(EXTRA_TRACK_ID, trackId);
}
@Override
@@ -247,19 +250,9 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.track_record, menu);
insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker);
markerListMenuItem = menu.findItem(R.id.track_detail_markers);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
updateMenuItems(recordingStatus.isPaused());
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
@@ -315,15 +308,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
return trackDataHub;
}
/**
* Updates the menu items.
*/
private void updateMenuItems(boolean isPaused) {
insertMarkerMenuItem.setVisible(!isPaused);
markerListMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
setTitle(getString(isPaused ? R.string.generic_paused : R.string.generic_recording));
}
@Override
public void onChooseActivityTypeDone(String iconValue) {
Track track = contentProviderUtils.getTrack(trackId);
@@ -331,29 +315,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils);
}
@Override
public void recordStart() {
updateMenuItems(false);
trackRecordingServiceConnection.resumeTrack();
}
@Override
public void recordPause() {
updateMenuItems(true);
trackRecordingServiceConnection.pauseTrack();
}
@Override
public void recordStop() {
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this);
Intent newIntent = IntentUtils.newIntent(TrackRecordingActivity.this, TrackRecordedActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
updateMenuItems(true);
finish();
}
private class CustomFragmentPagerAdapter extends FragmentStateAdapter {
public CustomFragmentPagerAdapter(@NonNull FragmentActivity fa) {
@@ -399,6 +360,9 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
private void onRecordingStatusChanged(RecordingStatus status) {
if (!status.isRecording()) {
finish();
}
recordingStatus = status;
trackDataHub.setRecordingStatus(recordingStatus);
@@ -412,17 +376,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
return;
}
ActivityResultLauncher<String[]> locationPermissionRequest = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
Boolean fineLocationGranted = result.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false);
Boolean coarseLocationGranted = result.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false);
if (fineLocationGranted == null || !fineLocationGranted
|| coarseLocationGranted == null || !coarseLocationGranted) {
Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show();
}
}
);
String[] permissions = new String[]{Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION};
locationPermissionRequest.launch(permissions);
PermissionUtils.requestGPSPermission(this, null, () -> Toast.makeText(this, R.string.permission_gps_failed, Toast.LENGTH_SHORT).show());
}
private void onGpsStatusChanged(GpsStatusValue gpsStatusValue) {
@@ -437,7 +391,8 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
.make(viewBinding.trackRecordingCoordinatorLayout,
getString(R.string.gps_recording_status, getString(gpsStatusValue.message), getString(R.string.gps_recording_without_signal)),
Snackbar.LENGTH_INDEFINITE)
.setAction(getString(R.string.generic_dismiss), v -> {});
.setAction(getString(R.string.generic_dismiss), v -> {
});
snackbar.show();
}
}
@@ -0,0 +1,137 @@
package de.dennisguse.opentracks;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.util.Pair;
import android.view.View;
import android.widget.ArrayAdapter;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.DistanceFormatter;
import de.dennisguse.opentracks.data.models.SpeedFormatter;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.databinding.TrackStoppedBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils;
public class TrackStoppedActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
private static final String TAG = TrackStoppedActivity.class.getSimpleName();
public static final String EXTRA_TRACK_ID = "track_id";
private TrackStoppedBinding viewBinding;
private Track.Id trackId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
if (trackId == null) {
Log.e(TAG, "TrackStoppedActivity needs EXTRA_TRACK_ID.");
finish();
}
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
Track track = contentProviderUtils.getTrack(trackId);
viewBinding.fields.trackEditName.setText(track.getName());
viewBinding.fields.trackEditActivityType.setText(track.getCategory());
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
viewBinding.fields.trackEditActivityType.setAdapter(adapter);
viewBinding.fields.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.fields.trackEditActivityType.getAdapter().getItem(position))));
viewBinding.fields.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackStoppedActivity.this, viewBinding.fields.trackEditActivityType.getText().toString()));
}
});
String iconValue = track.getIcon();
setActivityTypeIcon(iconValue);
viewBinding.fields.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.fields.trackEditActivityType.getText().toString()));
viewBinding.fields.trackEditDescription.setText(track.getDescription());
viewBinding.time.setText(StringUtils.formatElapsedTime(track.getTrackStatistics().getMovingTime()));
{
Pair<String, String> parts = SpeedFormatter.Builder()
.setUnit(PreferencesUtils.getUnitSystem())
.setReportSpeedOrPace(PreferencesUtils.isReportSpeed(track.getCategory()))
.build(this)
.getSpeedParts(track.getTrackStatistics().getAverageMovingSpeed());
viewBinding.speed.setText(parts.first);
viewBinding.speedUnit.setText(parts.second);
}
{
Pair<String, String> parts = DistanceFormatter.Builder()
.setUnit(PreferencesUtils.getUnitSystem())
.build(this)
.getDistanceParts(track.getTrackStatistics().getTotalDistance());
viewBinding.distance.setText(parts.first);
viewBinding.distanceUnit.setText(parts.second);
}
viewBinding.finishButton.setOnClickListener(v -> {
TrackUtils.updateTrack(TrackStoppedActivity.this, track, viewBinding.fields.trackEditName.getText().toString(),
viewBinding.fields.trackEditActivityType.getText().toString(), viewBinding.fields.trackEditDescription.getText().toString(),
contentProviderUtils);
ExportUtils.postWorkoutExport(this, trackId);
finish();
});
viewBinding.resumeButton.setOnClickListener(v -> {
resumeTrackAndFinish();
});
}
@Override
public void onBackPressed() {
super.onBackPressed();
resumeTrackAndFinish();
}
@Override
protected View getRootView() {
viewBinding = TrackStoppedBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
}
private void setActivityTypeIcon(String iconValue) {
viewBinding.fields.trackEditActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
}
@Override
public void onChooseActivityTypeDone(String iconValue) {
setActivityTypeIcon(iconValue);
viewBinding.fields.trackEditActivityType.setText(getString(TrackIconUtils.getIconActivityType(iconValue)));
}
private void resumeTrackAndFinish() {
new TrackRecordingServiceConnection((service, connection) -> {
service.resumeTrack(trackId);
Intent newIntent = IntentUtils.newIntent(TrackStoppedActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
connection.unbind(this);
finish();
}).startAndBind(this);
}
}
@@ -59,13 +59,13 @@ public class ShareContentProvider extends CustomContentProvider {
private static final String TRACKID_DELIMITER = "_";
static {
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.GPX.getName() + "/*/*", URI_GPX);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.GPX.getPreferenceId() + "/*/*", URI_GPX);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*/*", URI_KML_WITH_TRACKDETAIL_SENSORDATA);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getPreferenceId() + "/*/*", URI_KML_WITH_TRACKDETAIL_SENSORDATA);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.CSV.getName() + "/*/*", URI_CSV);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.getPreferenceId() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getPreferenceId() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES);
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.CSV.getPreferenceId() + "/*/*", URI_CSV);
}
/**
@@ -80,7 +80,7 @@ public class ShareContentProvider extends CustomContentProvider {
/**
* @return An URI for one file containing all tracks.
*/
public static Pair<Uri, String> createURI(Set<Track.Id> trackIds, String trackName, @NonNull TrackFileFormat trackFileFormat) {
public static Pair<Uri, String> createURI(Set<Track.Id> trackIds, String filename, @NonNull TrackFileFormat trackFileFormat) {
if (trackIds.isEmpty()) {
throw new UnsupportedOperationException();
}
@@ -91,7 +91,7 @@ public class ShareContentProvider extends CustomContentProvider {
}
trackIdBuilder.deleteCharAt(trackIdBuilder.lastIndexOf(TRACKID_DELIMITER));
Uri uri = Uri.parse(TracksColumns.CONTENT_URI + "/" + trackFileFormat.getName() + "/" + trackIdBuilder + "/" + Uri.encode(trackName) + "." + trackFileFormat.getExtension());
Uri uri = Uri.parse(TracksColumns.CONTENT_URI + "/" + trackFileFormat.getPreferenceId() + "/" + trackIdBuilder + "/" + Uri.encode(filename));
String mime = getTypeMime(uri);
Log.d(TAG, "Created uri " + uri + " with MIME " + mime);
@@ -77,7 +77,7 @@ public class StatisticsRecordingFragment extends Fragment {
}
};
private final TrackRecordingServiceConnection.Callback bindChangedCallback = service -> {
private final TrackRecordingServiceConnection.Callback bindChangedCallback = (service, unused) -> {
service.getRecordingDataObservable()
.observe(StatisticsRecordingFragment.this, this::onRecordingDataChanged);
};
@@ -2,8 +2,6 @@ package de.dennisguse.opentracks.io.file;
import android.content.Context;
import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.exporter.CSVTrackExporter;
@@ -19,7 +17,7 @@ import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
*/
public enum TrackFileFormat {
KML_WITH_TRACKDETAIL_AND_SENSORDATA {
KML_WITH_TRACKDETAIL_AND_SENSORDATA("KML_WITH_TRACKDETAIL_AND_SENSORDATA") {
@Override
public TrackExporter createTrackExporter(Context context) {
return new KMLTrackExporter(context, false);
@@ -36,7 +34,7 @@ public enum TrackFileFormat {
},
@Deprecated //TODO Check if we really need this
KMZ_WITH_TRACKDETAIL_AND_SENSORDATA {
KMZ_WITH_TRACKDETAIL_AND_SENSORDATA("KMZ_WITH_TRACKDETAIL_AND_SENSORDATA") {
private static final boolean exportPhotos = false;
@@ -61,7 +59,7 @@ public enum TrackFileFormat {
}
},
KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES {
KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES("KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES") {
private static final boolean exportPhotos = true;
@@ -87,7 +85,7 @@ public enum TrackFileFormat {
},
GPX {
GPX("GPX") {
@Override
public TrackExporter createTrackExporter(Context context) {
return new GPXTrackExporter(new ContentProviderUtils(context), context.getString(R.string.app_name));
@@ -103,7 +101,7 @@ public enum TrackFileFormat {
}
},
CSV {
CSV("CSV") {
@Override
public TrackExporter createTrackExporter(Context context) {
return new CSVTrackExporter(new ContentProviderUtils(context));
@@ -124,6 +122,12 @@ public enum TrackFileFormat {
private static final String MIME_KML = "application/vnd.google-earth.kml+xml";
private final String preferenceId;
TrackFileFormat(String preferenceId) {
this.preferenceId = preferenceId;
}
/**
* Returns the mime type for each format.
*/
@@ -149,9 +153,9 @@ public enum TrackFileFormat {
}
/**
* Returns the name for each format.
* The identifier to be stored in the preferences.
*/
public String getName() {
return this.name().toLowerCase(Locale.US);
public String getPreferenceId() {
return preferenceId;
}
}
@@ -21,10 +21,11 @@ public class TrackFilenameGenerator {
public static final String UUID_KEY = "{uuid}";
public static final String TRACKNAME_KEY = "{name}";
public static final String CATEGORY_KEY = "{category}";
public static final String STARTTIME_KEY = "{time}";
public static final String STARTTIME_TIME_KEY = "{time}";
public static final String STARTTIME_DATE_KEY = "{date}";
public static String getAllOptions() {
return Stream.of(UUID_KEY, TRACKNAME_KEY, CATEGORY_KEY, STARTTIME_KEY)
return Stream.of(UUID_KEY, TRACKNAME_KEY, CATEGORY_KEY, STARTTIME_TIME_KEY, STARTTIME_DATE_KEY)
.collect(Collectors.joining(", "));
}
@@ -40,7 +41,8 @@ public class TrackFilenameGenerator {
values.put(UUID_KEY, track.getUuid().toString().substring(0, 8));
values.put(TRACKNAME_KEY, track.getName());
values.put(CATEGORY_KEY, track.getCategory());
values.put(STARTTIME_KEY, track.getStartTime().toString());
values.put(STARTTIME_TIME_KEY, track.getStartTime().toLocalTime().toString());
values.put(STARTTIME_DATE_KEY, track.getStartTime().toLocalDate().toString());
return FileUtils.sanitizeFileName(format(template, values)) + "." + trackFileFormat.getExtension();
}
@@ -83,7 +85,7 @@ public class TrackFilenameGenerator {
try {
getExample();
return !template.isEmpty();
} catch (TemplateInvalidException e) {
} catch (TemplateInvalidException | NullPointerException e) {
return false;
}
}
@@ -94,7 +96,7 @@ public class TrackFilenameGenerator {
track.setUuid(UUID.fromString("fefefefefe-0000-1000-8000-00805f9b34fb"));
track.getTrackStatistics().setStartTime(Instant.ofEpochMilli(0));
return format(track, TrackFileFormat.GPX);
return format(track, TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES);
}
public static class TemplateInvalidException extends RuntimeException {
@@ -97,7 +97,7 @@ public class CSVTrackExporter implements TrackExporter {
new Column("time", null),
new Column("trackpoint_type", t -> quote(t.getType().name())),
new Column("latitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLatitude()) : ""),
new Column("longitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLatitude()) : ""),
new Column("longitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLongitude()) : ""),
new Column("altitude", t -> t.hasAltitude() ? COORDINATE_FORMAT.format(t.getAltitude().toM()) : ""),
new Column("accuracy_horizontal", t -> t.hasHorizontalAccuracy() ? DISTANCE_FORMAT.format(t.getHorizontalAccuracy().toM()) : ""),
new Column("accuracy_vertical", t -> t.hasVerticalAccuracy() ? DISTANCE_FORMAT.format(t.getVerticalAccuracy().toM()) : ""),
@@ -37,7 +37,6 @@ import de.dennisguse.opentracks.data.ContentProviderUtils;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.data.tables.TracksColumns;
import de.dennisguse.opentracks.databinding.ExportActivityBinding;
import de.dennisguse.opentracks.databinding.ToolbarBinding;
import de.dennisguse.opentracks.io.file.ErrorListDialog;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.ExportUtils;
@@ -135,7 +134,6 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewBinding = ExportActivityBinding.inflate(getLayoutInflater());
ToolbarBinding mergedBinding = ToolbarBinding.bind(viewBinding.getRoot());
setContentView(viewBinding.getRoot());
directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
@@ -146,8 +144,6 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
DocumentFile documentFile = DocumentFile.fromTreeUri(this, directoryUri);
String directoryDisplayName = FileUtils.getPath(documentFile);
mergedBinding.toolbar.setTitle(getString(R.string.export_progress_message, directoryDisplayName));
resultReceiver = new ExportServiceResultReceiver(new Handler(), this);
if (savedInstanceState == null) {
@@ -170,6 +166,9 @@ public class ExportActivity extends FragmentActivity implements ExportServiceRes
setProgress();
initExport(getTotalDone());
}
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.export_progress_message, directoryDisplayName));
viewBinding.bottomAppBarLayout.bottomAppBar.setNavigationIcon(R.drawable.ic_logo_color_24dp);
}
@Override
@@ -25,7 +25,6 @@ import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
@@ -71,8 +70,6 @@ public class ImportActivity extends FragmentActivity {
viewBinding = ImportActivityBinding.inflate(getLayoutInflater());
setContentView(viewBinding.getRoot());
Toolbar toolbar = findViewById(R.id.toolbar);
final Intent intent = getIntent();
final ClipData intentClipData = intent.getClipData();
@@ -106,9 +103,6 @@ public class ImportActivity extends FragmentActivity {
documentFiles = documentUris.stream().map(it -> DocumentFile.fromSingleUri(this, it)).collect(Collectors.toList());
}
//Works for a directory, but we might have received multiple files via SEND_MULTIPLE.
toolbar.setTitle(getString(R.string.import_progress_message, documentFiles.get(0).getName()));
initViews();
viewModel = new ViewModelProvider(this).get(ImportViewModel.class);
@@ -116,6 +110,10 @@ public class ImportActivity extends FragmentActivity {
summary = data;
setProgress();
});
//Works for a directory, but we might have received multiple files via SEND_MULTIPLE.
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.import_progress_message, documentFiles.get(0).getName()));
viewBinding.bottomAppBarLayout.bottomAppBar.setNavigationIcon(R.drawable.ic_logo_color_24dp);
}
@Override
@@ -16,27 +16,25 @@ public abstract class AbstractAPIActivity extends AppCompatActivity {
private final String TAG = AbstractAPIActivity.class.getSimpleName();
private final TrackRecordingServiceConnection.Callback serviceConnectedCallback = service -> {
private final TrackRecordingServiceConnection.Callback serviceConnectedCallback = (service, connection) -> {
if (!isFinishing() && !isDestroyed()) {
execute(service);
}
if (isPostExecuteStopService()) {
AbstractAPIActivity.this.trackRecordingServiceConnection.unbindAndStop(AbstractAPIActivity.this);
connection.unbindAndStop(AbstractAPIActivity.this);
} else {
AbstractAPIActivity.this.trackRecordingServiceConnection.unbind(AbstractAPIActivity.this);
connection.unbind(AbstractAPIActivity.this);
}
finish();
};
private TrackRecordingServiceConnection trackRecordingServiceConnection;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (PreferencesUtils.isPublicAPIenabled()) {
Log.i(TAG, "Received and trying to execute requested action.");
trackRecordingServiceConnection = new TrackRecordingServiceConnection(serviceConnectedCallback);
trackRecordingServiceConnection.startAndBind(this);
new TrackRecordingServiceConnection(serviceConnectedCallback)
.startAndBind(this);
} else {
Toast.makeText(this, getString(R.string.settings_public_api_disabled_toast), Toast.LENGTH_LONG).show();
Log.w(TAG, "Public API is disabled; ignoring request.");
@@ -44,12 +42,6 @@ public abstract class AbstractAPIActivity extends AppCompatActivity {
}
}
@Override
protected void onDestroy() {
super.onDestroy();
trackRecordingServiceConnection = null;
}
protected abstract void execute(TrackRecordingService service);
protected abstract boolean isPostExecuteStopService();
@@ -1,11 +1,24 @@
package de.dennisguse.opentracks.publicapi;
import de.dennisguse.opentracks.data.models.Track;
import de.dennisguse.opentracks.services.RecordingData;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.util.ExportUtils;
public class StopRecording extends AbstractAPIActivity {
protected void execute(TrackRecordingService service) {
RecordingData recordingData = service.getRecordingDataObservable().getValue();
Track.Id trackId = null;
if (recordingData != null && recordingData.getTrack() != null) {
trackId = recordingData.getTrack().getId();
}
service.endCurrentTrack();
if (trackId != null) {
ExportUtils.postWorkoutExport(this, trackId);
}
}
@Override
@@ -24,6 +24,8 @@ import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -110,7 +112,12 @@ public abstract class BluetoothConnectionManager<DataType> {
SensorData<DataType> sensorData = parsePayload(gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
if (sensorData != null) {
Log.d(TAG, "Decoded data from " + gatt.getDevice().getAddress() + ": " + sensorData);
observer.onChanged(sensorData);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
observer.onChanged(sensorData);
} else {
//TODO This might lead to NPEs in case of race conditions due to shutdown.
observer.getHandler().post(() -> observer.onChanged(sensorData));
}
}
}
};
@@ -128,7 +135,11 @@ public abstract class BluetoothConnectionManager<DataType> {
Log.d(TAG, "Connecting to: " + device);
bluetoothGatt = device.connectGatt(context, true, connectCallback);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
bluetoothGatt = device.connectGatt(context, true, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, this.observer.getHandler());
} else {
bluetoothGatt = device.connectGatt(context, true, connectCallback);
}
SensorData<?> sensorData = createEmptySensorData(bluetoothGatt.getDevice().getAddress());
observer.onChanged(sensorData);
}
@@ -275,5 +286,8 @@ public abstract class BluetoothConnectionManager<DataType> {
void onChanged(SensorData<?> sensorData);
void onDisconnecting(SensorData<?> sensorData);
@NonNull
Handler getHandler();
}
}
@@ -20,6 +20,7 @@ import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -34,6 +35,7 @@ import de.dennisguse.opentracks.sensors.sensorData.SensorDataCycling;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataRunning;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionUtils;
/**
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
@@ -57,7 +59,7 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
private final BluetoothAdapter bluetoothAdapter;
private final Context context;
private final Handler handler;
private boolean started = false;
private Distance preferenceWheelCircumference;
@@ -111,8 +113,9 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
}
};
public BluetoothRemoteSensorManager(@NonNull Context context, @NonNull SensorDataSetChangeObserver observer) {
public BluetoothRemoteSensorManager(@NonNull Context context, @NonNull Handler handler, @NonNull SensorDataSetChangeObserver observer) {
this.context = context;
this.handler = handler;
this.observer = observer;
bluetoothAdapter = BluetoothUtils.getAdapter(context);
}
@@ -159,6 +162,9 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
} else {
connectionManager.disconnect();
}
if (!PermissionUtils.hasBluetoothPermissions(context)) {
Log.w(TAG, "BLUETOOTH_SCAN and/or BLUETOOTH_CONNECT not granted; not connecting.");
}
Log.i(TAG, "Connecting to bluetooth address: " + address);
try {
@@ -218,6 +224,12 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
sensorDataSet.remove(sensorData);
}
@NonNull
@Override
public Handler getHandler() {
return handler;
}
public interface SensorDataSetChangeObserver {
void onChange(SensorDataSet sensorDataSet);
}
@@ -19,6 +19,7 @@ package de.dennisguse.opentracks.services;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import android.util.Pair;
@@ -39,7 +40,6 @@ import de.dennisguse.opentracks.services.announcement.VoiceAnnouncementManager;
import de.dennisguse.opentracks.services.handlers.EGM2008CorrectionManager;
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
import de.dennisguse.opentracks.util.ExportUtils;
import de.dennisguse.opentracks.util.SystemUtils;
public class TrackRecordingService extends Service implements TrackPointCreator.Callback {
@@ -102,7 +102,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
public void onCreate() {
super.onCreate();
handler = new Handler();
handler = new Handler(Looper.getMainLooper());
recordingStatusObservable = new MutableLiveData<>();
updateRecordingStatus(STATUS_DEFAULT);
@@ -212,7 +212,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
private void startSensors() {
wakeLock = SystemUtils.acquireWakeLock(this, wakeLock);
trackPointCreator.start(this);
trackPointCreator.start(this, handler);
showNotification(true);
}
@@ -235,8 +235,6 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
endRecording(true);
ExportUtils.postWorkoutExport(this, trackId);
stopSelf();
}
@@ -92,15 +92,10 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
return;
}
if (callback != null) {
callback.onConnected(trackRecordingService);
callback.onConnected(trackRecordingService, this);
}
}
/**
* Resumes the track recording service connection.
*
* @param context the context
*/
public void startConnection(@NonNull Context context) {
if (trackRecordingService != null) {
// Service is already started and bound.
@@ -115,6 +110,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
/**
* Unbinds the service (but leave it running).
*/
//TODO This is often called for one-shot operations and should be refactored as unbinding is required.
public void unbind(Context context) {
try {
context.unbindService(this);
@@ -141,7 +137,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
trackRecordingService = value;
if (callback != null) {
if (value != null) {
callback.onConnected(value);
callback.onConnected(value, this);
} else {
callback.onDisconnected();
}
@@ -214,7 +210,7 @@ public class TrackRecordingServiceConnection implements ServiceConnection, Death
}
public interface Callback {
void onConnected(TrackRecordingService service);
void onConnected(TrackRecordingService service, TrackRecordingServiceConnection connection);
default void onDisconnected() {
}
@@ -2,6 +2,7 @@ package de.dennisguse.opentracks.services.handlers;
import android.content.Context;
import android.location.Location;
import android.os.Handler;
import android.util.Log;
import android.util.Pair;
@@ -48,12 +49,12 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
this.gpsHandler = gpsHandler;
}
public synchronized void start(@NonNull Context context) {
public synchronized void start(@NonNull Context context, @NonNull Handler handler) {
this.context = context;
gpsHandler.onStart(context);
remoteSensorManager = new BluetoothRemoteSensorManager(context, this);
remoteSensorManager = new BluetoothRemoteSensorManager(context, handler, this);
altitudeSumManager = new AltitudeSumManager();
remoteSensorManager.start();
@@ -61,7 +61,7 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
String trackFileFormatUpperCase = trackFileFormat.getExtension().toUpperCase(Locale.US); //ASCII upper case
int photoMessageId = trackFileFormat.includesPhotos() ? R.string.export_with_photos : R.string.export_without_photos;
entries[i] = String.format("%s (%s)", trackFileFormatUpperCase, getString(photoMessageId));
entryValues[i] = trackFileFormat.name();
entryValues[i] = trackFileFormat.getPreferenceId();
}
ListPreference listPreference = findPreference(getString(R.string.export_trackfileformat_key));
@@ -18,7 +18,7 @@ public class MainSettingsFragment extends PreferenceFragmentCompat {
private RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private final TrackRecordingServiceConnection.Callback bindServiceCallback =
service -> service.getRecordingStatusObservable()
(service, unused) -> service.getRecordingStatusObservable()
.observe(MainSettingsFragment.this, this::onRecordingStatusChanged);
@Override
@@ -68,12 +68,6 @@ public class MainSettingsFragment extends PreferenceFragmentCompat {
});
}
@Override
public void onStart() {
super.onStart();
((SettingsActivity) getActivity()).getSupportActionBar().setTitle(R.string.menu_settings);
}
@Override
public void onResume() {
super.onResume();
@@ -201,7 +201,7 @@ public class PreferencesUtils {
final String VALUE = getString(R.string.stats_units_key, STATS_UNIT_DEFAULT);
return Arrays.stream(UnitSystem.values())
.filter(d -> VALUE.equals(getString(d.getPreferenceId(), STATS_UNIT_DEFAULT)))
.filter(d -> VALUE.equals(resources.getString(d.getPreferenceId(), STATS_UNIT_DEFAULT)))
.findFirst()
.orElse(UnitSystem.defaultUnitSystem()); //TODO This AGAIN defines the default
}
@@ -575,7 +575,7 @@ public class PreferencesUtils {
}
public static TrackFilenameGenerator getTrackFileformatGenerator() {
String DEFAULT = getString(R.string.export_filename_format_default, null);
String DEFAULT = resources.getString(R.string.export_filename_format_default);
TrackFilenameGenerator generator = new TrackFilenameGenerator(getString(R.string.export_filename_format_key, DEFAULT));
if (generator.isValid()) {
return generator;
@@ -587,11 +587,9 @@ public class PreferencesUtils {
public static TrackFileFormat getExportTrackFileFormat() {
final String TRACKFILEFORMAT_NAME_DEFAULT = getString(R.string.export_trackfileformat_default, null);
String trackFileFormatName = getString(R.string.export_trackfileformat_key, TRACKFILEFORMAT_NAME_DEFAULT);
try {
return TrackFileFormat.valueOf(trackFileFormatName);
} catch (Exception e) {
return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA;
}
return Arrays.stream(TrackFileFormat.values())
.filter(format -> format.getPreferenceId().equals(trackFileFormatName))
.findFirst().orElse(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES);
}
public static boolean getPreventReimportTracks() {
@@ -10,6 +10,8 @@ import androidx.preference.PreferenceFragmentCompat;
import de.dennisguse.opentracks.AbstractActivity;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.databinding.SettingsBinding;
import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
public class SettingsActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller {
@@ -21,6 +23,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
private PreferenceFragmentCompat fragment = null;
private SettingsBinding viewBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -37,6 +41,9 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
fragment = new MainSettingsFragment();
}
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.menu_settings));
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
getSupportFragmentManager().beginTransaction().replace(R.id.settings_fragment, fragment).commit();
}
@@ -66,7 +73,8 @@ public class SettingsActivity extends AbstractActivity implements ChooseActivity
@Override
protected View getRootView() {
return getLayoutInflater().inflate(R.layout.settings, null);
viewBinding = SettingsBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
}
private PreferenceFragmentCompat getPreferenceScreen(String key) {
@@ -6,7 +6,6 @@ import android.widget.ScrollView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
@@ -15,12 +14,12 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.stream.IntStream;
import de.dennisguse.opentracks.AbstractActivity;
import de.dennisguse.opentracks.ui.util.ArrayAdapterFilterDisabled;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.databinding.ActivitySettingsCustomLayoutBinding;
import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
import de.dennisguse.opentracks.ui.customRecordingLayout.SettingsCustomLayoutEditAdapter;
import de.dennisguse.opentracks.ui.util.ArrayAdapterFilterDisabled;
import de.dennisguse.opentracks.util.StatisticsUtils;
public class SettingsCustomLayoutEditActivity extends AbstractActivity implements SettingsCustomLayoutEditAdapter.SettingsCustomLayoutItemClickListener {
@@ -97,7 +96,8 @@ public class SettingsCustomLayoutEditActivity extends AbstractActivity implement
recyclerViewNotVisible.setLayoutManager(new LinearLayoutManager(this));
recyclerViewNotVisible.setAdapter(adapterFieldsHidden);
setTitle(profile);
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(profile);
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
}
@Override
@@ -117,12 +117,6 @@ public class SettingsCustomLayoutEditActivity extends AbstractActivity implement
layoutFieldsVisible = null;
}
@Override
protected void setupActionBarBack(Toolbar toolbar) {
super.setupActionBarBack(toolbar);
toolbar.setTitle(R.string.menu_settings);
}
@Override
protected View getRootView() {
viewBinding = ActivitySettingsCustomLayoutBinding.inflate(getLayoutInflater());
@@ -40,8 +40,6 @@ public class SettingsCustomLayoutListActivity extends AbstractActivity implement
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(getString(R.string.custom_layout_list_title));
adapter = new SettingsCustomLayoutListAdapter(this, this);
recyclerView = viewBinding.recyclerView;
recyclerView.setLayoutManager(new LinearLayoutManager(this));
@@ -114,6 +112,9 @@ public class SettingsCustomLayoutListActivity extends AbstractActivity implement
ItemTouchHelper itemTouchhelper = new ItemTouchHelper(recyclerViewSwipeDeleteCallback);
itemTouchhelper.attachToRecyclerView(recyclerView);
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.custom_layout_list_title));
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
}
@Override
@@ -1,6 +1,5 @@
package de.dennisguse.opentracks.settings.bluetooth;
import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
@@ -9,8 +8,6 @@ import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelUuid;
import android.text.TextUtils;
@@ -18,10 +15,7 @@ import android.util.AttributeSet;
import android.util.Log;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.preference.DialogPreference;
import androidx.preference.PreferenceDialogFragmentCompat;
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
@@ -35,6 +29,7 @@ import java.util.stream.Collectors;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.sensors.BluetoothUtils;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionUtils;
/**
* Preference to select a discoverable Bluetooth LE device.
@@ -150,26 +145,17 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
bluetoothIcon = AnimatedVectorDrawableCompat.create(getContext(), R.drawable.ic_bluetooth_searching_animated_24dp);
bluetoothIcon.start();
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && (
ContextCompat.checkSelfPermission(getContext(), Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(getContext(), Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED
)
) {
ActivityResultLauncher<String[]> requestPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), granted -> {
if (!granted.containsValue(false)) {
startBluetoothScan();
} else if (shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_SCAN) || shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) {
if (!PermissionUtils.hasBluetoothPermissions(getContext())) {
PermissionUtils.requestBluetoothPermission(this,
this::startBluetoothScan,
() -> {
if (PermissionUtils.shouldShowRequestPermissionRationaleBluetooth(this)) {
Toast.makeText(getContext(), R.string.permission_bluetooth_failed_rejected, Toast.LENGTH_LONG).show();
dismiss();
} else {
Toast.makeText(getContext(), R.string.permission_bluetooth_failed, Toast.LENGTH_SHORT).show();
dismiss();
}
dismiss();
});
requestPermissionLauncher.launch(new String[]{Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN});
} else {
startBluetoothScan();
}
@@ -60,6 +60,9 @@ public class AggregatedStatisticsActivity extends AbstractActivity implements Fi
}
adapter.notifyDataSetChanged();
});
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.menu_aggregated_statistics));
}
@Override
@@ -105,7 +105,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
@Override
public void onPageSelected(int position) {
setTitle(markerAdapter.getPageTitle(position));
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(markerAdapter.getPageTitle(position));
}
@Override
@@ -113,6 +113,8 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
}
});
viewBinding.makerDetailActivityViewPager.setCurrentItem(markerIndex == -1 ? 0 : markerIndex);
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
}
@Override
@@ -101,7 +101,7 @@ public class MarkerEditActivity extends AbstractActivity {
});
boolean isNewMarker = markerId == null;
setTitle(isNewMarker ? R.string.menu_insert_marker : R.string.menu_edit);
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(isNewMarker ? R.string.menu_insert_marker : R.string.menu_edit));
viewBinding.markerEditDone.setText(isNewMarker ? R.string.generic_add : R.string.generic_save);
viewBinding.markerEditDone.setOnClickListener(v -> {
viewModel.onDone(viewBinding.markerEditName.getText().toString(),
@@ -150,6 +150,8 @@ public class MarkerEditActivity extends AbstractActivity {
viewBinding.markerEditDescription.getText().toString());
}
});
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
}
@Override
@@ -71,7 +71,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private final TrackRecordingServiceConnection.Callback bindCallback = service -> service.getRecordingStatusObservable()
private final TrackRecordingServiceConnection.Callback bindCallback = (service, unused) -> service.getRecordingStatusObservable()
.observe(MarkerListActivity.this, this::onRecordingStatusChanged);
// Callback when an item is selected in the contextual action mode
@@ -80,6 +80,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isSingleSelection = ids.length == 1;
viewBinding.bottomAppBarLayout.bottomAppBar.performHide(true);
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection);
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
menu.findItem(R.id.list_context_menu_delete).setVisible(true);
@@ -92,6 +94,11 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
public boolean onClick(int itemId, int[] positions, long[] ids) {
return handleContextItem(itemId, ids);
}
@Override
public void onDestroy() {
viewBinding.bottomAppBarLayout.bottomAppBar.performShow(true);
}
};
private MenuItem insertMarkerMenuItem;
private MenuItem searchMenuItem;
@@ -121,6 +128,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
ActivityUtils.configureListViewContextualMenu(viewBinding.markerList, contextualActionModeCallback);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindCallback);
setSupportActionBar(viewBinding.bottomAppBarLayout.bottomAppBar);
}
@Override
@@ -301,9 +310,9 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
this.searchQuery = searchQuery;
restart();
if (searchQuery != null) {
setTitle(searchQuery);
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(searchQuery);
} else {
setTitle(R.string.menu_markers);
viewBinding.bottomAppBarLayout.bottomAppBarTitle.setText(getString(R.string.menu_markers));
}
}
@@ -105,7 +105,7 @@ public class MarkerResourceCursorAdapter extends ResourceCursorAdapter implement
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
Track track = contentProviderUtils.getTrack(new Track.Id(trackId));
ListItemUtils.setListItem(activity, view, false, true, iconId, R.string.image_marker, name, null, null, 0,
ListItemUtils.setListItem(activity, view, false, iconId, R.string.image_marker, name, null, null, 0,
OffsetDateTime.ofInstant(Instant.ofEpochMilli(time), track.getZoneOffset()), category, description, hasPhoto);
}
@@ -45,7 +45,7 @@ public class ActivityUtils {
@Override
public void onDestroyActionMode(ActionMode mode) {
// Do nothing
contextualActionModeCallback.onDestroy();
}
@Override
@@ -127,5 +127,10 @@ public class ActivityUtils {
* @param ids the selected items' ids, if available
*/
boolean onClick(int itemId, int[] positions, long[] ids);
/**
* Invoked when contextual action mode is destroyed.
*/
void onDestroy();
}
}
@@ -44,7 +44,6 @@ public class ListItemUtils {
* @param context the context
* @param view the list item view
* @param isRecording true if recording
* @param isPaused true if paused
* @param iconId the icon id
* @param iconContentDescriptionId the icon content description id
* @param name the name value
@@ -56,11 +55,11 @@ public class ListItemUtils {
* @param description the description value
* @param hasPhoto true if this list item has photo
*/
public static void setListItem(Context context, View view, boolean isRecording, boolean isPaused, int iconId, int iconContentDescriptionId, String name, String totalTime, String totalDistance, int markerCount, OffsetDateTime offsetDateTime, String category, String description, boolean hasPhoto) {
public static void setListItem(Context context, View view, boolean isRecording, int iconId, int iconContentDescriptionId, String name, String totalTime, String totalDistance, int markerCount, OffsetDateTime offsetDateTime, String category, String description, boolean hasPhoto) {
// Set icon
if (isRecording) {
iconId = isPaused ? R.drawable.ic_track_paused : R.drawable.ic_track_recording;
iconContentDescriptionId = isPaused ? R.string.image_pause : R.string.image_record;
iconId = R.drawable.ic_track_recording;
iconContentDescriptionId = R.string.image_record;
}
ImageView iconImageView = view.findViewById(R.id.list_item_icon);
@@ -75,7 +74,7 @@ public class ListItemUtils {
TextView timeDistanceTextView = view.findViewById(R.id.list_item_time_distance);
String timeDistanceText;
if (isRecording) {
timeDistanceText = context.getString(isPaused ? R.string.generic_paused : R.string.generic_recording);
timeDistanceText = context.getString(R.string.generic_recording);
} else {
// Match list_item_time_distance in list_item.xml
timeDistanceText = getTimeDistance(totalTime, totalDistance);
@@ -0,0 +1,80 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.annotation.NonNull;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.ShowErrorActivity;
public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
private final Context context;
private final Thread.UncaughtExceptionHandler defaultExceptionHandler;
public ExceptionHandler(final Context context, final Thread.UncaughtExceptionHandler defaultExceptionHandler) {
this.context = context;
this.defaultExceptionHandler = defaultExceptionHandler;
}
@Override
public void uncaughtException(@NonNull final Thread thread, @NonNull final Throwable exception) {
try {
String errorReport = generateErrorReport(formatException(thread, exception));
Intent intent = new Intent(context, ShowErrorActivity.class);
intent.putExtra(ShowErrorActivity.EXTRA_ERROR_TEXT, errorReport);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
// Pass exception to OS for graceful handling - OS will report it via ADB
// and close all activities and services.
defaultExceptionHandler.uncaughtException(thread, exception);
} catch (final Exception fatalException) {
// do not recurse into custom handler if exception is thrown during
// exception handling. Pass this ultimate fatal exception to OS
defaultExceptionHandler.uncaughtException(thread, fatalException);
}
}
private String formatException(final Thread thread, final Throwable exception) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(String.format("Exception in thread \"%s\": ", thread.getName()));
// print available stacktrace
Writer writer = new StringWriter();
exception.printStackTrace(new PrintWriter(writer));
stringBuilder.append(writer);
return stringBuilder.toString();
}
private String generateErrorReport(final String stackTrace) {
return
"### App information\n" +
"* ID: " + BuildConfig.APPLICATION_ID + "\n" +
"* Version: " + BuildConfig.VERSION_CODE + " " + BuildConfig.VERSION_NAME + "\n" +
"\n" +
"### Device information\n" +
"* Brand: " + Build.BRAND + "\n" +
"* Device: " + Build.DEVICE + "\n" +
"* Model: " + Build.MODEL + "\n" +
"* Id: " + Build.ID + "\n" +
"* Product: " + Build.PRODUCT + "\n" +
"\n" +
"### Firmware\n" +
"* SDK: " + Build.VERSION.SDK_INT + "\n" +
"* Release: " + Build.VERSION.RELEASE + "\n" +
"* Incremental: " + Build.VERSION.INCREMENTAL + "\n" +
"\n" +
"### Cause of error\n" +
"```java\n" +
stackTrace +
"```\n";
}
}
@@ -3,16 +3,72 @@ package de.dennisguse.opentracks.util;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.activity.result.ActivityResultCaller;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import java.util.Arrays;
public class PermissionUtils {
private static final String[] GPS = new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION};
private static final String[] BLUETOOTH;
static {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
BLUETOOTH = new String[]{};
} else {
BLUETOOTH = new String[]{Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT};
}
}
private PermissionUtils() {
}
public static boolean hasGPSPermission(Context context) {
return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
return hasPermissions(context, GPS);
}
public static void requestGPSPermission(ActivityResultCaller context, @Nullable Runnable onGranted, @Nullable Runnable onRejected) {
requestPermission(context, GPS, onGranted, onRejected);
}
public static boolean shouldShowRequestPermissionRationaleBluetooth(Fragment context) {
return Arrays.stream(BLUETOOTH).anyMatch(context::shouldShowRequestPermissionRationale);
}
public static boolean hasBluetoothPermissions(Context context) {
return hasPermissions(context, BLUETOOTH);
}
public static void requestBluetoothPermission(ActivityResultCaller context, @Nullable Runnable onGranted, @Nullable Runnable onRejected) {
requestPermission(context, BLUETOOTH, onGranted, onRejected);
}
private static void requestPermission(ActivityResultCaller context, final String[] permissions, @Nullable Runnable onGranted, @Nullable Runnable onRejected) {
ActivityResultLauncher<String[]> locationPermissionRequest = context.registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
boolean isGranted = Arrays.stream(permissions)
.allMatch(p -> result.getOrDefault(p, false));
if (isGranted && onGranted != null) {
onGranted.run();
}
if (!isGranted && onRejected != null) {
onRejected.run();
}
}
);
locationPermissionRequest.launch(permissions);
}
private static boolean hasPermissions(Context context, String[] permissions) {
return Arrays.stream(permissions)
.map(p -> ContextCompat.checkSelfPermission(context, p))
.allMatch(r -> r == PackageManager.PERMISSION_GRANTED);
}
}
@@ -6,4 +6,9 @@
android:inset="20dp"
android:drawable="@drawable/ic_logo_color_24dp" />
</foreground>
<monochrome>
<inset
android:inset="20dp"
android:drawable="@drawable/ic_logo_24dp" />
</monochrome>
</adaptive-icon>
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_button_record_pressed" android:state_pressed="true" />
<item android:drawable="@drawable/ic_button_record" />
</selector>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_button_stop_disabled" android:state_enabled="false" />
<item android:drawable="@drawable/ic_button_stop_pressed" android:state_pressed="true" />
<item android:drawable="@drawable/ic_button_stop" />
</selector>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<transition xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The drawables used here can be solid colors, gradients, shapes, images, etc. -->
<item android:drawable="@color/track_controller_background_normal" />
<item android:drawable="@color/track_controller_background_activated" />
</transition>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M6,6h12v12H6z"/>
</vector>
@@ -0,0 +1,6 @@
<vector android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="48" android:viewportWidth="48"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M24,42C19.871,42.036 15.525,39.899 13.8,36L8,36v-3h4.6c-0.498,-1.726 -0.314,-3.53 -0.35,-5.3h-4.3v-3h4.3c-0.169,-1.827 0.064,-4.169 0.293,-5.667L8,19v-3h6c0.913,-1.868 2.567,-3.276 4.4,-4.2L14.55,8l2,-2 4.7,4.7c2.672,-0.682 3.902,-0.511 5.855,-0.205L31.6,6l2,2 -3.8,3.8c1.847,0.901 3.312,2.447 4.35,4.2h5.9v3L35.3,19c0.482,0.673 0.569,4.047 0.689,5.7L40.05,24.7v3L35.7,27.7c0.127,2.006 -0.004,4.014 -0.242,5.3h4.592v3h-5.8c-1.661,4.014 -6.109,6.018 -10.25,6zM24,39c4.625,0.162 8.864,-4.009 8.7,-8.65 -0.034,-3.149 0.073,-6.304 -0.062,-9.449 -0.439,-4.459 -4.853,-8.007 -9.304,-7.529 -4.488,0.196 -8.327,4.419 -8.034,8.917 0.034,3.053 -0.072,6.111 0.062,9.159C15.79,35.668 19.764,39.128 24,39ZM20,32h8v-3h-8zM20,23.35h8v-3h-8zM24,26.2c0.015,0 0.026,0 0,0z"/>
</vector>
+23
View File
@@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="125dp"
android:height="125dp"
android:viewportWidth="125"
android:viewportHeight="125">
<path
android:pathData="M62.5,62.5m-59.984,0a59.984,59.984 0,1 1,119.968 0a59.984,59.984 0,1 1,-119.968 0"
android:strokeWidth="5.03185"
android:fillColor="#00000000"
android:strokeColor="#ff6d00"/>
<path
android:pathData="M62.5,62.5m-50,0a50,50 0,1 1,100 0a50,50 0,1 1,-100 0"
android:strokeWidth="6.91728"
android:fillColor="#ff6d00"
android:strokeColor="#00000000"/>
<path
android:pathData="M38.62,63.866 L52.899,78.144 86.38,44.664"
android:strokeLineJoin="miter"
android:strokeWidth="6.2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="butt"/>
</vector>
@@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="125dp"
android:height="125dp"
android:viewportWidth="125"
android:viewportHeight="125">
<path
android:pathData="M62.5,62.5m-59.984,0a59.984,59.984 0,1 1,119.968 0a59.984,59.984 0,1 1,-119.968 0"
android:strokeWidth="5.03185"
android:fillColor="#00000000"
android:strokeColor="#ff6d00"/>
<path
android:pathData="M62.5,62.5m-50,0a50,50 0,1 1,100 0a50,50 0,1 1,-100 0"
android:strokeWidth="6.91728"
android:fillColor="#ff6d00"
android:strokeColor="#00000000"/>
<path
android:pathData="M87.6,62.5 L67.991,73.822 48.381,85.143l-0,-22.643 0,-22.643 19.61,11.322z"
android:strokeWidth="6.67087"
android:fillColor="#ffffff"
android:strokeColor="#00000000"
android:strokeLineCap="round"/>
</vector>
+5 -5
View File
@@ -7,16 +7,16 @@
<path android:pathData="M82.489,12.834C83.196,10.418 82.043,9.556 79.926,10.917L10.087,55.828c-2.117,1.361 -1.795,2.628 0.714,2.815L39.806,60.807c2.51,0.188 5.291,2.267 6.18,4.621l10.282,27.208c0.89,2.354 2.195,2.304 2.902,-0.111z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="81.73018"
android:endY="11.204678"
android:startX="27.696081"
android:startY="83.44198"
android:endX="70.0"
android:endY="30.0"
android:startX="7.0"
android:startY="83.0"
android:type="linear">
<item
android:color="#FFFBB22F"
android:offset="0" />
<item
android:color="#FFF58820"
android:color="#FFFF6D00"
android:offset="1" />
</gradient>
</aapt:attr>
-16
View File
@@ -1,16 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillAlpha="1"
android:fillColor="#a31a1f"
android:pathData="M15,14h5v20h-5z"
android:strokeWidth="1.01154423" />
<path
android:fillAlpha="1"
android:fillColor="#a31a1f"
android:pathData="M27,14h5v20h-5z"
android:strokeWidth="1.01154423" />
</vector>
@@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/controller_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/controller_background"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<ImageButton
android:id="@+id/controller_record"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_record"
android:src="@drawable/ic_button_record" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="6">
<TextView
android:id="@+id/controller_total_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?attr/textAppearanceSubtitle2"
android:layout_centerInParent="true"
android:textColor="@android:color/white" />
<TextView
android:id="@+id/controller_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?attr/textAppearanceSubtitle2"
android:layout_above="@id/controller_total_time"
android:layout_centerHorizontal="true"
android:singleLine="true" />
</RelativeLayout>
<ImageButton
android:id="@+id/controller_stop"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="10"
android:background="@android:color/transparent"
android:contentDescription="@string/image_stop"
android:src="@drawable/button_stop" />
</LinearLayout>
-100
View File
@@ -1,100 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="vertical">
<include layout="@layout/toolbar" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/controller_fragment"
android:name="de.dennisguse.opentracks.ControllerFragment"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
<ListView
android:id="@+id/track_list"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0" />
<RelativeLayout
android:id="@+id/track_list_empty_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.0">
<LinearLayout
android:id="@+id/track_list_arrow_long"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingEnd="16dp">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="10"
android:contentDescription="@string/image_arrow"
android:src="@drawable/ic_arrow_long" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="17" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toEndOf="@id/track_list_arrow_long"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="4" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="24">
<TextView
style="?attr/textAppearanceHeadline6"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/track_list_empty_message" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
</LinearLayout>
@@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/controller_fragment"
android:name="de.dennisguse.opentracks.ControllerFragment"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/track_detail_activity_tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/track_recording_coordinator_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/track_detail_activity_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
+105 -97
View File
@@ -1,117 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:layout_height="match_parent">
<include layout="@layout/toolbar" />
<ScrollView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="50dp">
<LinearLayout
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_margin="8dp">
android:orientation="vertical"
android:padding="20dp">
<ImageView
android:id="@+id/imageView"
android:layout_width="30dp"
android:layout_height="match_parent"
android:contentDescription="@string/app_name"
android:scaleType="fitCenter"
android:src="@drawable/ic_logo_color_24dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_margin="8dp">
<ImageView
android:id="@+id/imageView"
android:layout_width="30dp"
android:layout_height="match_parent"
android:contentDescription="@string/app_name"
android:scaleType="fitCenter"
android:src="@drawable/ic_logo_color_24dp" />
<TextView
android:id="@+id/about_text_description"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:gravity="center"
android:padding="12dp" />
</LinearLayout>
<View style="@style/HorizontalLine" />
<TextView
android:id="@+id/about_text_description"
android:id="@+id/about_text_version_name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:gravity="center"
android:padding="12dp" />
android:padding="6dp" />
<TextView
android:id="@+id/about_text_version_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody1"
android:gravity="center"
android:padding="6dp" />
<TextView
android:id="@+id/about_app_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody1"
android:autoLink="web"
android:gravity="center"
android:padding="6dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody1"
android:gravity="center"
android:linksClickable="true"
android:padding="6dp"
android:text="@string/about_history" />
<View style="@style/HorizontalLine" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody1"
android:gravity="center"
android:padding="12dp"
android:text="@string/about_third_party_data" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody1"
android:gravity="center"
android:padding="12dp"
android:text="@string/third_party_data" />
<View style="@style/HorizontalLine" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:gravity="center"
android:padding="12dp"
android:text="@string/about_libraries" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody1"
android:gravity="center"
android:linksClickable="true"
android:text="@string/third_party_libraries" />
</LinearLayout>
</ScrollView>
<View style="@style/HorizontalLine" />
</LinearLayout>
<TextView
android:id="@+id/about_text_version_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:gravity="center"
android:padding="6dp" />
<include android:id="@+id/bottom_app_bar_layout" layout="@layout/bottomappbar" />
<TextView
android:id="@+id/about_text_version_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody1"
android:gravity="center"
android:padding="6dp" />
<TextView
android:id="@+id/about_app_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody1"
android:autoLink="web"
android:gravity="center"
android:padding="6dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody1"
android:gravity="center"
android:linksClickable="true"
android:padding="6dp"
android:text="@string/about_history" />
<View style="@style/HorizontalLine" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody1"
android:gravity="center"
android:padding="12dp"
android:text="@string/about_third_party_data" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody1"
android:gravity="center"
android:padding="12dp"
android:text="@string/third_party_data" />
<View style="@style/HorizontalLine" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:gravity="center"
android:padding="12dp"
android:text="@string/about_libraries" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceBody1"
android:gravity="center"
android:linksClickable="true"
android:text="@string/third_party_libraries" />
</LinearLayout>
</ScrollView>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

Some files were not shown because too many files have changed in this diff Show More