Compare commits

...

50 Commits

Author SHA1 Message Date
Dennis Guse a42bdf32e1 Release: v3.11.1 2020-10-06 08:16:58 +02:00
Dennis Guse c29d69b44d Cleanup. 2020-10-05 22:59:21 +02:00
Román 547b1d07bd Bugfix: interval stats announced incorrectly because an error in last interval computing. Fixes #443. 2020-10-05 20:08:07 +02:00
Dennis Guse 22a1dfd354 README: removed link to Maps.ME due to the amount of embedded trackers. 2020-10-04 12:51:14 +02:00
Dennis Guse be81e758c6 License: use non-Markdown, so Github-API identifies it. 2020-09-26 23:16:19 +02:00
Dennis Guse b3117e6410 Release: v3.11.0 2020-09-26 20:23:32 +02:00
Dennis Guse 47ab109c2b F-Droid now signs APKs with v2, so installing on Android 11 works.
More info: #421
This reverts commit ee22fd8e
2020-09-26 19:56:22 +02:00
Dennis Guse bc04efbaac Bugfix: for export all write permissions are required.
Android 11 enforces this (and it is good practice).
Fixes #439.
2020-09-26 19:52:48 +02:00
Román fbb9d21e73 TrackRecordingActivity keeps Track.Id value instead of track object. Every time the track is needed it gets it. Fixes #435. 2020-09-26 16:47:51 +02:00
dependabot[bot] f5308e4287 Bump mockito-android from 3.5.11 to 3.5.13
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.5.11 to 3.5.13.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.5.11...v3.5.13)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-25 18:20:32 +02:00
Dennis Guse 15b5920e3b README: add F-Droid version shield 2020-09-25 12:37:58 +02:00
Dennis Guse d09aaab398 Theming: import/export finished dialog needs to be AndroidX. 2020-09-24 22:45:20 +02:00
pappapisshu f7008e2362 Update strings.xml
Fixed italian translation.
2020-09-24 17:16:54 +02:00
Dennis Guse e939778beb Refactor: use marker instead of waypoint.
Fixes #389.
2020-09-23 17:16:34 +02:00
Dennis Guse 8643b50bd1 Bugfix: application context does not provide access to theme attributes. 2020-09-23 12:12:34 +02:00
Dennis Guse 74e42525d4 Cleanup. 2020-09-23 08:11:10 +02:00
Dennis Guse 833a387574 Theming: splashscreen uses dark background color. 2020-09-23 08:05:40 +02:00
Dennis Guse e574c67ad6 Theming: activity icons are now in textColorPrimary.
And replaced spinner in StatisticsRecordedFragment.
2020-09-23 07:36:15 +02:00
Dennis Guse ce38d5e891 Theming: define colorAccent. 2020-09-23 07:36:15 +02:00
Dennis Guse 3a44da4a9d Theming: stats labels use color accent. 2020-09-23 07:36:15 +02:00
Dennis Guse eba9106db5 Theming: delete dialog needs to be Androidx for proper theming. 2020-09-23 07:36:15 +02:00
Dennis Guse 4501226562 Theming: reduce font size of lists first line. 2020-09-23 07:36:15 +02:00
Dennis Guse b974193564 Theming: delete dialog uses delete icon. 2020-09-23 07:36:15 +02:00
Dennis Guse 0b3e447258 Theming: charts use theme text colors. 2020-09-23 07:36:15 +02:00
Dennis Guse 9908cc02a2 Theming: added color.xml for night styling. 2020-09-23 07:36:15 +02:00
Dennis Guse 4e67011d7c Theming: style selected list items with transparent color. 2020-09-23 07:36:15 +02:00
Dennis Guse 5f93c0bab3 Theming: tabs use default indicator color. 2020-09-23 07:36:15 +02:00
Dennis Guse caec5e7c45 Theming: chart use default design background color. 2020-09-23 07:36:15 +02:00
Dennis Guse 240cb4b363 Theming: chart colors extracted. 2020-09-23 07:36:15 +02:00
Dennis Guse 91e8eb66b1 Theming: chart text is now rendered as FILL_AND_STROKE. 2020-09-23 07:36:15 +02:00
Dennis Guse 27bd69ec62 Code cleanup. 2020-09-23 07:36:15 +02:00
Dennis Guse 553fd39e21 Theming: make SplashScreen fullscreen. 2020-09-23 07:36:15 +02:00
Dennis Guse 733ee1c6e6 Theming: move TrackEdit to material design text fields. 2020-09-23 07:36:15 +02:00
Dennis Guse ef61c9597d Theming: activity chooser matches parent height. 2020-09-23 07:36:15 +02:00
Dennis Guse e874af791e Theming: removed all custom modifications to Holo theme. 2020-09-23 07:36:15 +02:00
Dennis Guse a4b2bb1aa0 Theming: tint all icons with colorControlNormal. 2020-09-23 07:36:15 +02:00
Dennis Guse bc9b20ffc8 Theming: marker detail uses default background. 2020-09-23 07:36:15 +02:00
Dennis Guse 6f212328a1 Theming: merged edit buttons into layout. 2020-09-23 07:36:15 +02:00
Dennis Guse a68cde7ac9 Theming: listView main item is now TextLarge. 2020-09-23 07:36:15 +02:00
Dennis Guse 5cab331058 Theming: removed custom spinner style. 2020-09-23 07:36:15 +02:00
Dennis Guse 6d25a10abd Theming: use default text color. 2020-09-23 07:36:15 +02:00
Dennis Guse d2516947f2 Theming: removed custom radio button style - use Material design. 2020-09-23 07:36:15 +02:00
Dennis Guse 4150d060ee Theming: enable day/night mode incl. app-wide setting. 2020-09-23 07:36:15 +02:00
Dennis Guse 1688076457 Theming: use AppCompat themes instead of Holo. 2020-09-23 07:36:15 +02:00
Román 5c1f669b2b Bugfix: spinner adapter needed notifyDataSetChanged to reload intervals with unit when user change from km to mi and the other way around. 2020-09-21 19:12:57 +02:00
Dennis Guse cc1e080ce1 Interval statistics: show unit in dropdown. 2020-09-20 19:05:32 +02:00
Dennis Guse 0dfd31a308 Code cleanup via Android Studio analyze.
Notably change: TrackPoint's copy constructor missed to copy the elevationGain.
2020-09-20 18:46:05 +02:00
dependabot[bot] bd13535d3c Bump mockito-android from 3.5.10 to 3.5.11
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.5.10 to 3.5.11.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.5.10...v3.5.11)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-18 08:07:00 +02:00
Dennis Guse 27386bb4f0 Localization: update via Transifex CLI. 2020-09-17 23:44:37 +02:00
Dennis Guse c6c07f03bf Localization: add configuration for Transifex CLI. 2020-09-17 23:43:28 +02:00
273 changed files with 2654 additions and 1376 deletions
+10
View File
@@ -0,0 +1,10 @@
[main]
host = https://www.transifex.com
[opentracks.src-main-res-values-strings-xml--main]
file_filter = src/main/res/values-<lang>/strings.xml
minimum_perc = 0
source_file = src/main/res/values/strings.xml
source_lang = en
type = ANDROID
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
-194
View File
@@ -1,194 +0,0 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
+3 -1
View File
@@ -13,6 +13,8 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
<td> <td>
<a href="https://f-droid.org/packages/de.dennisguse.opentracks"> <a href="https://f-droid.org/packages/de.dennisguse.opentracks">
<img alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="60" align="middle"> <img alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="60" align="middle">
<br />
<img alt="OpenTracks version published on F-Droid" src="https://img.shields.io/f-droid/v/de.dennisguse.opentracks.svg" align="middle" >
</a> </a>
</td> </td>
<td> <td>
@@ -61,7 +63,7 @@ As of v3.3.1 OpenTracks enables to use custom dashboards.
The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates). The reference implementation is [OSMDashboard](https://github.com/OpenTracksApp/OSMDashboard), which presents an OpenStreetMap map (showing the current track, incl. updates).
The developer documentation is in [README_API.md](README_API.md). The developer documentation is in [README_API.md](README_API.md).
Alternatively, recorded tracks can be shared as KMZ/GPX to installed applications (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand) or [MAPS.ME](https://play.google.com/store/apps/details?id=com.mapswithme.maps.pro)). Alternatively, recorded tracks can be shared as KMZ/GPX to installed applications (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand)).
However, this is rather slow and does not provide updates while recording. However, this is rather slow and does not provide updates while recording.
+4 -4
View File
@@ -47,15 +47,15 @@ android {
defaultConfig { defaultConfig {
applicationId "de.dennisguse.opentracks" applicationId "de.dennisguse.opentracks"
versionCode 3611 versionCode 3661
versionName "v3.10.0" versionName "v3.11.1"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\"" buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 30
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -94,5 +94,5 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'org.mockito:mockito-android:3.5.10' androidTestImplementation 'org.mockito:mockito-android:3.5.13'
} }
@@ -0,0 +1,10 @@
v3.11.0: OpenTracks
Changes:
- Theming: day/night mode (default: night)
- Theming: use Material Design instead of custom element theming
- Use targetSDK 30 again (F-Droid signs now with V2)
Bugfix:
- Android 11: export all crashed
- TrackDetail not updated in UI after data was changed by user
@@ -0,0 +1,4 @@
v3.11.1: OpenTracks
Bugfix:
- Voice announcements reported wrong statistics
@@ -76,7 +76,7 @@ public class ExportImportTest {
marker.setName("the marker " + i); marker.setName("the marker " + i);
marker.setDescription("the marker description " + i); marker.setDescription("the marker description " + i);
marker.setCategory("the marker category" + i); marker.setCategory("the marker category" + i);
marker.setIcon("the waypoing icon" + i); marker.setIcon("the marker icon" + i);
marker.setPhotoUrl("the photo url" + i); marker.setPhotoUrl("the photo url" + i);
marker.setTrackId(trackId); marker.setTrackId(trackId);
contentProviderUtils.insertMarker(marker); contentProviderUtils.insertMarker(marker);
@@ -318,7 +318,7 @@ public class TrackRecordingServiceTest {
// then // then
assertNotEquals(-1L, waypointId); assertNotEquals(-1L, waypointId);
Marker wpt = contentProviderUtils.getMarker(waypointId); Marker wpt = contentProviderUtils.getMarker(waypointId);
assertEquals(context.getString(R.string.marker_waypoint_icon_url), wpt.getIcon()); assertEquals(context.getString(R.string.marker_icon_url), wpt.getIcon());
assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName()); assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
assertEquals(trackId, wpt.getTrackId()); assertEquals(trackId, wpt.getTrackId());
assertEquals(0.0, wpt.getLength(), 0.01); assertEquals(0.0, wpt.getLength(), 0.01);
@@ -23,7 +23,7 @@ public class AggregatedStatisticsActivity extends AbstractActivity {
final AggregatedStatisticsModel viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class); final AggregatedStatisticsModel viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class);
viewModel.getAggregatedStats().observe(this, aggregatedStatistics -> { viewModel.getAggregatedStats().observe(this, aggregatedStatistics -> {
if (aggregatedStatistics != null) { if (aggregatedStatistics != null) {
adapter = new AggregatedStatisticsAdapter(getApplicationContext(), aggregatedStatistics); adapter = new AggregatedStatisticsAdapter(this, aggregatedStatistics);
listView.setAdapter(adapter); listView.setAdapter(adapter);
} }
adapter.notifyDataSetChanged(); adapter.notifyDataSetChanged();
@@ -48,33 +48,33 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
private static final String TAG = MarkerDetailActivity.class.getSimpleName(); private static final String TAG = MarkerDetailActivity.class.getSimpleName();
private List<Marker.Id> waypointIds; private List<Marker.Id> markerIds;
@Override @Override
protected void onCreate(Bundle bundle) { protected void onCreate(Bundle bundle) {
super.onCreate(bundle); super.onCreate(bundle);
Marker.Id waypointId = getIntent().getParcelableExtra(EXTRA_MARKER_ID); Marker.Id markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
if (waypointId == null) { if (markerId == null) {
Log.d(TAG, "invalid marker id"); Log.d(TAG, "invalid marker id");
finish(); finish();
return; return;
} }
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
Marker waypoint = contentProviderUtils.getMarker(waypointId); Marker marker = contentProviderUtils.getMarker(markerId);
waypointIds = new ArrayList<>(); markerIds = new ArrayList<>();
int markerIndex = -1; int markerIndex = -1;
//TODO Load only waypointIds, not the whole waypoint //TODO Load only markerIds, not the whole marker
try (Cursor cursor = contentProviderUtils.getMarkerCursor(waypoint.getTrackId(), null, -1)) { try (Cursor cursor = contentProviderUtils.getMarkerCursor(marker.getTrackId(), null, -1)) {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
for (int i = 0; i < cursor.getCount(); i++) { for (int i = 0; i < cursor.getCount(); i++) {
Marker currentMarker = contentProviderUtils.createMarker(cursor); Marker currentMarker = contentProviderUtils.createMarker(cursor);
waypointIds.add(currentMarker.getId()); markerIds.add(currentMarker.getId());
if (waypointId.equals(currentMarker.getId())) { if (markerId.equals(currentMarker.getId())) {
markerIndex = waypointIds.size() - 1; markerIndex = markerIds.size() - 1;
} }
cursor.moveToNext(); cursor.moveToNext();
@@ -127,7 +127,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
@Override @Override
@NonNull @NonNull
public Fragment getItem(int position) { public Fragment getItem(int position) {
return MarkerDetailFragment.newInstance(waypointIds.get(position)); return MarkerDetailFragment.newInstance(markerIds.get(position));
} }
@Nullable @Nullable
@@ -138,7 +138,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
@Override @Override
public int getCount() { public int getCount() {
return waypointIds.size(); return markerIds.size();
} }
} }
} }
@@ -32,12 +32,13 @@ import android.view.View;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView; import android.widget.AutoCompleteTextView;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.android.material.textfield.TextInputEditText;
import java.io.File; import java.io.File;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
@@ -67,7 +68,7 @@ public class MarkerEditActivity extends AbstractActivity {
private static final String TAG = MarkerEditActivity.class.getSimpleName(); private static final String TAG = MarkerEditActivity.class.getSimpleName();
private Track.Id trackId; private Track.Id trackId;
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection(); private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
private Marker waypoint; private Marker marker;
private MenuItem insertPhotoMenuItem; private MenuItem insertPhotoMenuItem;
private MenuItem insertGalleryImgMenuItem; private MenuItem insertGalleryImgMenuItem;
@@ -76,35 +77,35 @@ public class MarkerEditActivity extends AbstractActivity {
private boolean hasCamera; private boolean hasCamera;
// UI elements // UI elements
private EditText waypointName; private TextInputEditText markerName;
private AutoCompleteTextView waypointMarkerType; private AutoCompleteTextView markerType;
private EditText waypointDescription; private TextInputEditText markerDescription;
private ImageView waypointPhoto; private ImageView markerPhoto;
private ImageView waypointDeletePhotoBtn; private ImageView markerDeletePhotoBtn;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID); trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
Marker.Id waypointId = getIntent().getParcelableExtra(EXTRA_MARKER_ID); Marker.Id markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
// Setup UI elements // Setup UI elements
waypointName = findViewById(R.id.marker_edit_waypoint_name); markerName = findViewById(R.id.marker_edit_name);
waypointMarkerType = findViewById(R.id.marker_edit_waypoint_marker_type); markerType = findViewById(R.id.marker_edit_marker_type);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.waypoint_types, android.R.layout.simple_dropdown_item_1line); ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.marker_types, android.R.layout.simple_dropdown_item_1line);
waypointMarkerType.setAdapter(adapter); markerType.setAdapter(adapter);
waypointDescription = findViewById(R.id.marker_edit_waypoint_description); markerDescription = findViewById(R.id.marker_edit_description);
waypointPhoto = findViewById(R.id.marker_edit_waypoint_photo); markerPhoto = findViewById(R.id.marker_edit_photo);
waypointDeletePhotoBtn = findViewById(R.id.marker_edit_waypoint_photo_delete); markerDeletePhotoBtn = findViewById(R.id.marker_edit_photo_delete);
waypointDeletePhotoBtn.setOnClickListener(v -> { markerDeletePhotoBtn.setOnClickListener(v -> {
if (waypoint != null && waypoint.hasPhoto()) { if (marker != null && marker.hasPhoto()) {
waypoint.setPhotoUrl(null); marker.setPhotoUrl(null);
} }
waypointPhoto.setImageBitmap(null); markerPhoto.setImageBitmap(null);
photoUri = null; photoUri = null;
hideAndShowOptions(); hideAndShowOptions();
}); });
@@ -112,7 +113,7 @@ public class MarkerEditActivity extends AbstractActivity {
Button cancel = findViewById(R.id.marker_edit_cancel); Button cancel = findViewById(R.id.marker_edit_cancel);
cancel.setOnClickListener(v -> finish()); cancel.setOnClickListener(v -> finish());
final boolean isNewMarker = waypointId == null; final boolean isNewMarker = markerId == null;
setTitle(isNewMarker ? R.string.menu_insert_marker : R.string.menu_edit); setTitle(isNewMarker ? R.string.menu_insert_marker : R.string.menu_edit);
Button done = findViewById(R.id.marker_edit_done); Button done = findViewById(R.id.marker_edit_done);
@@ -127,37 +128,37 @@ public class MarkerEditActivity extends AbstractActivity {
}); });
if (isNewMarker) { if (isNewMarker) {
int nextWaypointNumber = trackId == null ? -1 : new ContentProviderUtils(this).getNextMarkerNumber(trackId); int nextMarkerNumber = trackId == null ? -1 : new ContentProviderUtils(this).getNextMarkerNumber(trackId);
if (nextWaypointNumber == -1) { if (nextMarkerNumber == -1) {
nextWaypointNumber = 0; nextMarkerNumber = 0;
} }
waypointName.setText(getString(R.string.marker_name_format, nextWaypointNumber)); markerName.setText(getString(R.string.marker_name_format, nextMarkerNumber));
waypointName.selectAll(); markerName.selectAll();
waypointMarkerType.setText(""); markerType.setText("");
waypointDescription.setText(""); markerDescription.setText("");
} else { } else {
waypoint = new ContentProviderUtils(this).getMarker(waypointId); marker = new ContentProviderUtils(this).getMarker(markerId);
if (waypoint == null) { if (marker == null) {
Log.d(TAG, "waypoint is null"); Log.d(TAG, "marker is null");
finish(); finish();
return; return;
} }
waypointName.setText(waypoint.getName()); markerName.setText(marker.getName());
waypointMarkerType.setText(waypoint.getCategory()); markerType.setText(marker.getCategory());
waypointDescription.setText(waypoint.getDescription()); markerDescription.setText(marker.getDescription());
if (waypoint.hasPhoto()) { if (marker.hasPhoto()) {
photoUri = waypoint.getPhotoURI(); photoUri = marker.getPhotoURI();
} }
} }
if (savedInstanceState != null) { if (savedInstanceState != null) {
photoUri = savedInstanceState.getParcelable(BUNDLE_PHOTO_URI); photoUri = savedInstanceState.getParcelable(BUNDLE_PHOTO_URI);
if (waypoint != null) { if (marker != null) {
waypoint.setPhotoUrl(photoUri != null ? photoUri.toString() : null); marker.setPhotoUrl(photoUri != null ? photoUri.toString() : null);
} }
} }
if (photoUri != null) { if (photoUri != null) {
setWaypointImageView(photoUri); setMarkerImageView(photoUri);
} }
hideAndShowOptions(); hideAndShowOptions();
@@ -202,10 +203,10 @@ public class MarkerEditActivity extends AbstractActivity {
public boolean onOptionsItemSelected(@NonNull MenuItem item) { public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.marker_edit_insert_photo: case R.id.marker_edit_insert_photo:
createWaypointWithPicture(); createMarkerWithPicture();
return true; return true;
case R.id.marker_edit_insert_gallery_img: case R.id.marker_edit_insert_gallery_img:
createWaypointWithGalleryImage(); createMarkerWithGalleryImage();
return true; return true;
default: default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
@@ -219,7 +220,7 @@ public class MarkerEditActivity extends AbstractActivity {
Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show();
return; return;
} else if (resultCode == RESULT_OK) { } else if (resultCode == RESULT_OK) {
setWaypointImageView(photoUri); setMarkerImageView(photoUri);
} }
} else if (requestCode == GALLERY_IMG_REQUEST_CODE) { } else if (requestCode == GALLERY_IMG_REQUEST_CODE) {
if (resultCode == RESULT_CANCELED) { if (resultCode == RESULT_CANCELED) {
@@ -233,7 +234,7 @@ public class MarkerEditActivity extends AbstractActivity {
FileUtils.copy(srcFd, dstFile); FileUtils.copy(srcFd, dstFile);
photoUri = FileUtils.getUriForFile(this, dstFile); photoUri = FileUtils.getUriForFile(this, dstFile);
setWaypointImageView(photoUri); setMarkerImageView(photoUri);
} catch(Exception e) { } catch(Exception e) {
Log.e(TAG, e.getMessage()); Log.e(TAG, e.getMessage());
Toast.makeText(this, R.string.marker_add_canceled, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.marker_add_canceled, Toast.LENGTH_LONG).show();
@@ -250,31 +251,26 @@ public class MarkerEditActivity extends AbstractActivity {
* If a photo is set then one's options are shown, otherwise another ones are shown. * If a photo is set then one's options are shown, otherwise another ones are shown.
*/ */
private void hideAndShowOptions() { private void hideAndShowOptions() {
boolean isPhotoSet = (waypoint != null && waypoint.hasPhoto()) || photoUri != null; boolean isPhotoSet = (marker != null && marker.hasPhoto()) || photoUri != null;
if (insertPhotoMenuItem != null && insertGalleryImgMenuItem != null) { if (insertPhotoMenuItem != null && insertGalleryImgMenuItem != null) {
insertPhotoMenuItem.setVisible(!isPhotoSet); insertPhotoMenuItem.setVisible(!isPhotoSet);
insertGalleryImgMenuItem.setVisible(!isPhotoSet); insertGalleryImgMenuItem.setVisible(!isPhotoSet);
} }
waypointDeletePhotoBtn.setVisibility(isPhotoSet ? View.VISIBLE : View.GONE); markerDeletePhotoBtn.setVisibility(isPhotoSet ? View.VISIBLE : View.GONE);
} }
/** /**
* Returns the trackId. If trackId == -1 then get it from waypoint object. * Returns the trackId; either from track or marker.
*/ */
private Track.Id getTrackId() { private Track.Id getTrackId() {
return trackId == null ? waypoint.getTrackId() : trackId; return trackId == null ? marker.getTrackId() : trackId;
} }
/** private void setMarkerImageView(@NonNull Uri uri) {
* Sets the ImageView waypointPhoto with the uri photo.
*
* @param uri the uri photo.
*/
private void setWaypointImageView(@NonNull Uri uri) {
try (ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r")) { try (ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r")) {
FileDescriptor fd = pfd.getFileDescriptor(); FileDescriptor fd = pfd.getFileDescriptor();
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd); Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd);
waypointPhoto.setImageBitmap(bitmap); markerPhoto.setImageBitmap(bitmap);
hideAndShowOptions(); hideAndShowOptions();
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, e.getMessage()); Log.e(TAG, e.getMessage());
@@ -282,31 +278,31 @@ public class MarkerEditActivity extends AbstractActivity {
} }
} }
private void createWaypointWithPicture() { private void createMarkerWithPicture() {
Pair<Intent, Uri> intentAndPhotoUri = IntentUtils.createTakePictureIntent(this, getTrackId()); Pair<Intent, Uri> intentAndPhotoUri = IntentUtils.createTakePictureIntent(this, getTrackId());
photoUri = intentAndPhotoUri.second; photoUri = intentAndPhotoUri.second;
startActivityForResult(intentAndPhotoUri.first, CAMERA_REQUEST_CODE); startActivityForResult(intentAndPhotoUri.first, CAMERA_REQUEST_CODE);
} }
private void createWaypointWithGalleryImage() { private void createMarkerWithGalleryImage() {
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, GALLERY_IMG_REQUEST_CODE); startActivityForResult(intent, GALLERY_IMG_REQUEST_CODE);
} }
private void addMarker() { private void addMarker() {
trackRecordingServiceConnection.addMarker(this, trackRecordingServiceConnection.addMarker(this,
waypointName.getText().toString(), markerName.getText().toString(),
waypointMarkerType.getText().toString(), markerType.getText().toString(),
waypointDescription.getText().toString(), markerDescription.getText().toString(),
photoUri != null ? photoUri.toString() : null); photoUri != null ? photoUri.toString() : null);
} }
private void saveMarker() { private void saveMarker() {
waypoint.setName(waypointName.getText().toString()); marker.setName(markerName.getText().toString());
waypoint.setCategory(waypointMarkerType.getText().toString()); marker.setCategory(markerType.getText().toString());
waypoint.setDescription(waypointDescription.getText().toString()); marker.setDescription(markerDescription.getText().toString());
waypoint.setPhotoUrl(photoUri != null ? photoUri.toString() : null); marker.setPhotoUrl(photoUri != null ? photoUri.toString() : null);
new ContentProviderUtils(this).updateMarker(this, waypoint); new ContentProviderUtils(this).updateMarker(this, marker);
} }
} }
@@ -230,34 +230,34 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
* Handles a context item selection. * Handles a context item selection.
* *
* @param itemId the menu item id * @param itemId the menu item id
* @param longWaypointIds the marker ids * @param longMarkerds the marker ids
* @return true if handled. * @return true if handled.
*/ */
private boolean handleContextItem(int itemId, long... longWaypointIds) { private boolean handleContextItem(int itemId, long... longMarkerds) {
Marker.Id[] waypointIds = new Marker.Id[longWaypointIds.length]; Marker.Id[] markerIds = new Marker.Id[longMarkerds.length];
for (int i = 0; i < longWaypointIds.length; i++) { for (int i = 0; i < longMarkerds.length; i++) {
waypointIds[i] = new Marker.Id(longWaypointIds[i]); markerIds[i] = new Marker.Id(longMarkerds[i]);
} }
Intent intent; Intent intent;
switch (itemId) { switch (itemId) {
case R.id.list_context_menu_show_on_map: case R.id.list_context_menu_show_on_map:
if (waypointIds.length == 1) { if (markerIds.length == 1) {
IntentUtils.showCoordinateOnMap(this, contentProviderUtils.getMarker(waypointIds[0])); IntentUtils.showCoordinateOnMap(this, contentProviderUtils.getMarker(markerIds[0]));
} }
return true; return true;
case R.id.list_context_menu_edit: case R.id.list_context_menu_edit:
if (waypointIds.length == 1) { if (markerIds.length == 1) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class) intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, waypointIds[0]); .putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerIds[0]);
startActivity(intent); startActivity(intent);
} }
return true; return true;
case R.id.list_context_menu_delete: case R.id.list_context_menu_delete:
if (waypointIds.length > 1 && waypointIds.length == listView.getCount()) { if (markerIds.length > 1 && markerIds.length == listView.getCount()) {
waypointIds = null; markerIds = null;
} }
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), waypointIds); DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), markerIds);
return true; return true;
case R.id.list_context_menu_select_all: case R.id.list_context_menu_select_all:
for (int i = 0; i < listView.getCount(); i++) { for (int i = 0; i < listView.getCount(); i++) {
@@ -271,7 +271,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
} }
Map<String, Object> item = arrayAdapter.getItem(positions[0]); Map<String, Object> item = arrayAdapter.getItem(positions[0]);
Track.Id trackId = (Track.Id) item.get(TRACK_ID_FIELD); Track.Id trackId = (Track.Id) item.get(TRACK_ID_FIELD);
Marker.Id waypointId = (Marker.Id) item.get(MARKER_ID_FIELD); Marker.Id markerId = (Marker.Id) item.get(MARKER_ID_FIELD);
Intent intent; Intent intent;
switch (itemId) { switch (itemId) {
case R.id.list_context_menu_show_on_map: case R.id.list_context_menu_show_on_map:
@@ -283,9 +283,9 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
startActivity(intent); startActivity(intent);
return true; return true;
case R.id.list_context_menu_edit: case R.id.list_context_menu_edit:
if (waypointId != null) { if (markerId != null) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class) intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, waypointId); .putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
} else { } else {
intent = IntentUtils.newIntent(this, TrackEditActivity.class) intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId); .putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
@@ -296,8 +296,8 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
finish(); finish();
return true; return true;
case R.id.list_context_menu_delete: case R.id.list_context_menu_delete:
if (waypointId != null) { if (markerId != null) {
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), waypointId); DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), markerId);
} else { } else {
deleteTracks(trackId); deleteTracks(trackId);
} }
@@ -349,7 +349,7 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
* @return a list of result maps * @return a list of result maps
*/ */
private List<Map<String, Object>> prepareResultsforDisplay(Collection<ScoredResult> scoredResults) { private List<Map<String, Object>> prepareResultsforDisplay(Collection<ScoredResult> scoredResults) {
//TODO Replace use of map<string, object>, but rather provide Track or Waypoint directly. //TODO Replace use of map<string, object>, but rather provide Track or Marker directly.
ArrayList<Map<String, Object>> output = new ArrayList<>(scoredResults.size()); ArrayList<Map<String, Object>> output = new ArrayList<>(scoredResults.size());
for (ScoredResult result : scoredResults) { for (ScoredResult result : scoredResults) {
if (result.track != null) { if (result.track != null) {
@@ -361,18 +361,12 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
return output; return output;
} }
/** private Map<String, Object> prepareMarkerForDisplay(Marker marker) {
* Prepares a marker for display by filling in a result map.
*
* @param waypoint the marker
* @return the result map
*/
private Map<String, Object> prepareMarkerForDisplay(Marker waypoint) {
Map<String, Object> resultMap = new HashMap<>(); Map<String, Object> resultMap = new HashMap<>();
//TODO: It may be more appropriate to obtain the track name as a join in the retrieval phase of the searchable. //TODO: It may be more appropriate to obtain the track name as a join in the retrieval phase of the searchable.
String trackName = null; String trackName = null;
Track.Id trackId = waypoint.getTrackId(); Track.Id trackId = marker.getTrackId();
if (!trackId.isValid()) { if (!trackId.isValid()) {
Track track = contentProviderUtils.getTrack(trackId); Track track = contentProviderUtils.getTrack(trackId);
if (track != null) { if (track != null) {
@@ -384,20 +378,20 @@ public class SearchListActivity extends AbstractListActivity implements DeleteMa
resultMap.put(IS_PAUSED_FIELD, true); resultMap.put(IS_PAUSED_FIELD, true);
resultMap.put(ICON_ID_FIELD, MarkerUtils.ICON_ID); resultMap.put(ICON_ID_FIELD, MarkerUtils.ICON_ID);
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_marker); resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_marker);
resultMap.put(NAME_FIELD, waypoint.getName()); resultMap.put(NAME_FIELD, marker.getName());
// Display the marker's track name in the total time field // Display the marker's track name in the total time field
resultMap.put(TOTAL_TIME_FIELD, trackName == null ? null : getString(R.string.search_list_marker_track_location, trackName)); resultMap.put(TOTAL_TIME_FIELD, trackName == null ? null : getString(R.string.search_list_marker_track_location, trackName));
resultMap.put(TOTAL_DISTANCE_FIELD, null); resultMap.put(TOTAL_DISTANCE_FIELD, null);
resultMap.put(MARKER_COUNT_FIELD, 0); resultMap.put(MARKER_COUNT_FIELD, 0);
resultMap.put(START_TIME_FIELD, waypoint.getLocation().getTime()); resultMap.put(START_TIME_FIELD, marker.getLocation().getTime());
resultMap.put(CATEGORY_FIELD, waypoint.getCategory()); resultMap.put(CATEGORY_FIELD, marker.getCategory());
resultMap.put(DESCRIPTION_FIELD, waypoint.getDescription()); resultMap.put(DESCRIPTION_FIELD, marker.getDescription());
resultMap.put(PHOTO_URL_FIELD, waypoint.getPhotoUrl()); resultMap.put(PHOTO_URL_FIELD, marker.getPhotoUrl());
resultMap.put(TRACK_ID_FIELD, waypoint.getTrackId()); resultMap.put(TRACK_ID_FIELD, marker.getTrackId());
resultMap.put(MARKER_ID_FIELD, waypoint.getId()); resultMap.put(MARKER_ID_FIELD, marker.getId());
resultMap.put(MARKER_LATITUDE_FIELD, waypoint.getLocation().getLatitude()); resultMap.put(MARKER_LATITUDE_FIELD, marker.getLocation().getLatitude());
resultMap.put(MARKER_LONGITUDE_FIELD, waypoint.getLocation().getLongitude()); resultMap.put(MARKER_LONGITUDE_FIELD, marker.getLocation().getLongitude());
return resultMap; return resultMap;
} }
@@ -6,6 +6,7 @@ import android.util.Log;
import java.util.Locale; import java.util.Locale;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
/** /**
@@ -32,6 +33,8 @@ public class Startup extends Application {
PreferencesUtils.setString(this, R.string.stats_units_key, statsUnits); PreferencesUtils.setString(this, R.string.stats_units_key, statsUnits);
} }
ActivityUtils.applyNightMode(this);
//TODO Workaround to reset recordingTrackId on app startup as the TrackRecordingService (likely) crashed. //TODO Workaround to reset recordingTrackId on app startup as the TrackRecordingService (likely) crashed.
if (PreferencesUtils.isRecording(this)) { if (PreferencesUtils.isRecording(this)) {
Log.e(TAG, "Reset recordingTrackId; likely the TrackRecordingService crashed."); Log.e(TAG, "Reset recordingTrackId; likely the TrackRecordingService crashed.");
@@ -24,11 +24,12 @@ import android.view.View;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView; import android.widget.AutoCompleteTextView;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner; import android.widget.Spinner;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.android.material.textfield.TextInputEditText;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment; import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
@@ -55,10 +56,10 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
private Track track; private Track track;
private String iconValue; private String iconValue;
private EditText nameEditText; private TextInputEditText nameEditText;
private AutoCompleteTextView activityType; private AutoCompleteTextView activityType;
private Spinner activityTypeIcon; private Spinner activityTypeIcon;
private EditText description; private TextInputEditText description;
@Override @Override
protected void onCreate(Bundle bundle) { protected void onCreate(Bundle bundle) {
@@ -55,11 +55,11 @@ import de.dennisguse.opentracks.util.TrackUtils;
*/ */
public class TrackRecordedActivity extends AbstractListActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface { public class TrackRecordedActivity extends AbstractListActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
public static final String EXTRA_TRACK_ID = "track_id"; public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id"; public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key"; private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
// The following are set in onCreate. // The following are set in onCreate.
@@ -67,7 +67,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
private TrackDataHub trackDataHub; private TrackDataHub trackDataHub;
private ViewPager pager; private ViewPager pager;
private Track track; private Track.Id trackId;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -100,8 +100,8 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
// Update UI // Update UI
this.invalidateOptionsMenu(); this.invalidateOptionsMenu();
if (track != null) { if (trackId != null) {
trackDataHub.loadTrack(track.getId()); trackDataHub.loadTrack(trackId);
} }
} }
@@ -139,6 +139,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
public boolean onPrepareOptionsMenu(Menu menu) { 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_markers).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
menu.findItem(R.id.track_detail_resume_track).setVisible(!PreferencesUtils.isRecording(this)); menu.findItem(R.id.track_detail_resume_track).setVisible(!PreferencesUtils.isRecording(this));
Track track = contentProviderUtils.getTrack(trackId);
setTitle(track != null ? track.getName() : ""); setTitle(track != null ? track.getName() : "");
return super.onPrepareOptionsMenu(menu); return super.onPrepareOptionsMenu(menu);
} }
@@ -148,29 +149,29 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
Intent intent; Intent intent;
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.track_detail_share: case R.id.track_detail_share:
intent = IntentUtils.newShareFileIntent(this, track.getId()); intent = IntentUtils.newShareFileIntent(this, trackId);
intent = Intent.createChooser(intent, null); intent = Intent.createChooser(intent, null);
startActivity(intent); startActivity(intent);
return true; return true;
case R.id.track_detail_menu_show_on_map: case R.id.track_detail_menu_show_on_map:
IntentDashboardUtils.startDashboard(this, false, track.getId()); IntentDashboardUtils.startDashboard(this, false, trackId);
return true; return true;
case R.id.track_detail_markers: case R.id.track_detail_markers:
intent = IntentUtils.newIntent(this, MarkerListActivity.class) intent = IntentUtils.newIntent(this, MarkerListActivity.class)
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, track.getId()); .putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent); startActivity(intent);
return true; return true;
case R.id.track_detail_edit: case R.id.track_detail_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class) intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, track.getId()); .putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent); startActivity(intent);
return true; return true;
case R.id.track_detail_delete: case R.id.track_detail_delete:
deleteTracks(track.getId()); deleteTracks(trackId);
return true; return true;
case R.id.track_detail_resume_track: case R.id.track_detail_resume_track:
Intent newIntent = IntentUtils.newIntent(TrackRecordedActivity.this, TrackRecordingActivity.class) Intent newIntent = IntentUtils.newIntent(TrackRecordedActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, track.getId()); .putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent); startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
finish(); finish();
@@ -204,25 +205,23 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
} }
private void handleIntent(Intent intent) { private void handleIntent(Intent intent) {
Track.Id trackId = intent.getParcelableExtra(EXTRA_TRACK_ID); trackId = intent.getParcelableExtra(EXTRA_TRACK_ID);
Marker.Id waypointId = intent.getParcelableExtra(EXTRA_MARKER_ID); Marker.Id markerId = intent.getParcelableExtra(EXTRA_MARKER_ID);
if (waypointId != null) { if (markerId != null) {
// Use the trackId from the marker // Use the trackId from the marker
Marker waypoint = contentProviderUtils.getMarker(waypointId); Marker marker = contentProviderUtils.getMarker(markerId);
if (waypoint == null) { if (marker == null) {
finish(); finish();
return; return;
} }
trackId = waypoint.getTrackId(); trackId = marker.getTrackId();
} }
if (trackId == null) { if (trackId == null) {
Log.e(TAG, "TrackDetailActivity needs EXTRA_TRACK_ID."); Log.e(TAG, "TrackDetailActivity needs EXTRA_TRACK_ID.");
finish(); finish();
} }
track = contentProviderUtils.getTrack(trackId);
} }
public void chooseActivityType(String category) { public void chooseActivityType(String category) {
@@ -232,6 +231,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
@Override @Override
public void onChooseActivityTypeDone(String iconValue) { public void onChooseActivityTypeDone(String iconValue) {
String category = getString(TrackIconUtils.getIconActivityType(iconValue)); String category = getString(TrackIconUtils.getIconActivityType(iconValue));
Track track = contentProviderUtils.getTrack(trackId);
TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils); TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils);
} }
@@ -251,9 +251,9 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
public Fragment getItem(int position) { public Fragment getItem(int position) {
switch (position) { switch (position) {
case 0: case 0:
return StatisticsRecordedFragment.newInstance(track.getId()); return StatisticsRecordedFragment.newInstance(trackId);
case 1: case 1:
return IntervalsFragment.newInstance(track.getId()); return IntervalsFragment.newInstance(trackId);
case 2: case 2:
return ChartFragment.newInstance(false); return ChartFragment.newInstance(false);
case 3: case 3:
@@ -72,7 +72,7 @@ public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.I
*/ */
public enum StackMode { public enum StackMode {
STACK_FROM_BOTTOM, STACK_FROM_BOTTOM,
STACK_FROM_TOP; STACK_FROM_TOP
} }
private static class ViewHolder { private static class ViewHolder {
@@ -47,9 +47,9 @@ abstract class ChartValueSeries {
private final Paint strokePaint; private final Paint strokePaint;
private final Paint titlePaint; private final Paint titlePaint;
private final Paint markerPaint; private final Paint markerPaint;
private final ExtremityMonitor extremityMonitor; private final ExtremityMonitor extremityMonitor = new ExtremityMonitor();
private final NumberFormat numberFormat; private final NumberFormat numberFormat = NumberFormat.getIntegerInstance();
private final Path path; private final Path path = new Path();
private int interval = 1; private int interval = 1;
private int minMarkerValue = 0; private int minMarkerValue = 0;
@@ -91,17 +91,15 @@ abstract class ChartValueSeries {
titlePaint = new Paint(strokePaint); titlePaint = new Paint(strokePaint);
titlePaint.setTextSize(ChartView.MEDIUM_TEXT_SIZE * scale); titlePaint.setTextSize(ChartView.MEDIUM_TEXT_SIZE * scale);
titlePaint.setTextAlign(Align.CENTER); titlePaint.setTextAlign(Align.CENTER);
titlePaint.setStyle(Style.FILL_AND_STROKE);
markerPaint = new Paint(strokePaint); markerPaint = new Paint(strokePaint);
markerPaint.setTextSize(ChartView.SMALL_TEXT_SIZE * scale); markerPaint.setTextSize(ChartView.SMALL_TEXT_SIZE * scale);
markerPaint.setTextAlign(Align.RIGHT); markerPaint.setTextAlign(Align.RIGHT);
markerPaint.setStyle(Style.FILL_AND_STROKE);
// Set stroke paint thickness // Set stroke paint thickness
strokePaint.setStrokeWidth(STROKE_WIDTH); strokePaint.setStrokeWidth(STROKE_WIDTH);
extremityMonitor = new ExtremityMonitor();
numberFormat = NumberFormat.getIntegerInstance();
path = new Path();
} }
/** /**
@@ -19,7 +19,6 @@ package de.dennisguse.opentracks.chart;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect; import android.graphics.DashPathEffect;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Paint.Align; import android.graphics.Paint.Align;
@@ -47,6 +46,7 @@ import de.dennisguse.opentracks.stats.ExtremityMonitor;
import de.dennisguse.opentracks.util.IntentUtils; import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.MarkerUtils; import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.ThemeUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
/** /**
@@ -75,7 +75,7 @@ public class ChartView extends View {
private static final int Y_AXIS_OFFSET = 16; private static final int Y_AXIS_OFFSET = 16;
//TODO Determine from actual size of the used drawable //TODO Determine from actual size of the used drawable
private static final float WAYPOINT_X_ANCHOR = 13f / 48f; private static final float MARKER_X_ANCHOR = 13f / 48f;
static { static {
X_FRACTION_FORMAT.setMaximumFractionDigits(1); X_FRACTION_FORMAT.setMaximumFractionDigits(1);
@@ -87,14 +87,15 @@ public class ChartView extends View {
private final ChartValueSeries paceSeries; private final ChartValueSeries paceSeries;
private final List<ChartPoint> chartPoints = new ArrayList<>(); private final List<ChartPoint> chartPoints = new ArrayList<>();
private final List<Marker> waypoints = new ArrayList<>(); private final List<Marker> markers = new ArrayList<>();
private final ExtremityMonitor xExtremityMonitor = new ExtremityMonitor(); private final ExtremityMonitor xExtremityMonitor = new ExtremityMonitor();
private final int backgroundColor;
private final Paint axisPaint; private final Paint axisPaint;
private final Paint xAxisMarkerPaint; private final Paint xAxisMarkerPaint;
private final Paint gridPaint; private final Paint gridPaint;
private final Paint markerPaint; private final Paint markerPaint;
private final Drawable pointer; private final Drawable pointer;
private final Drawable waypointMarker; private final Drawable markerPin;
private final int markerWidth; private final int markerWidth;
private final int markerHeight; private final int markerHeight;
private final Scroller scroller; private final Scroller scroller;
@@ -150,19 +151,19 @@ public class ChartView extends View {
// Check if the y event is within markerHeight of the marker center // Check if the y event is within markerHeight of the marker center
if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2f) < markerHeight) { if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2f) < markerHeight) {
int minDistance = Integer.MAX_VALUE; int minDistance = Integer.MAX_VALUE;
Marker nearestWaypoint = null; Marker nearestMarker = null;
synchronized (waypoints) { synchronized (markers) {
for (Marker waypoint : waypoints) { for (Marker marker : markers) {
int distance = Math.abs(getX(getWaypointXValue(waypoint)) - (int) event.getX() - getScrollX()); int distance = Math.abs(getX(getMarkerXValue(marker)) - (int) event.getX() - getScrollX());
if (distance < minDistance) { if (distance < minDistance) {
minDistance = distance; minDistance = distance;
nearestWaypoint = waypoint; nearestMarker = marker;
} }
} }
} }
if (nearestWaypoint != null && minDistance < markerWidth) { if (nearestMarker != null && minDistance < markerWidth) {
Intent intent = IntentUtils.newIntent(getContext(), MarkerDetailActivity.class) Intent intent = IntentUtils.newIntent(getContext(), MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, nearestWaypoint.getId()); .putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, nearestMarker.getId());
getContext().startActivity(intent); getContext().startActivity(intent);
return true; return true;
} }
@@ -191,7 +192,6 @@ public class ChartView extends View {
public ChartView(Context context, final boolean chartByDistance) { public ChartView(Context context, final boolean chartByDistance) {
super(context); super(context);
this.chartByDistance = chartByDistance; this.chartByDistance = chartByDistance;
seriesList.add(new ChartValueSeries(context, seriesList.add(new ChartValueSeries(context,
Integer.MIN_VALUE, Integer.MIN_VALUE,
Integer.MAX_VALUE, Integer.MAX_VALUE,
@@ -307,11 +307,13 @@ public class ChartView extends View {
} }
}); });
backgroundColor = ThemeUtils.getBackgroundColor(context);
float scale = context.getResources().getDisplayMetrics().density; float scale = context.getResources().getDisplayMetrics().density;
axisPaint = new Paint(); axisPaint = new Paint();
axisPaint.setStyle(Style.STROKE); axisPaint.setStyle(Style.FILL_AND_STROKE);
axisPaint.setColor(context.getResources().getColor(android.R.color.black)); axisPaint.setColor(ThemeUtils.getTextColorPrimary(context));
axisPaint.setAntiAlias(true); axisPaint.setAntiAlias(true);
axisPaint.setTextSize(SMALL_TEXT_SIZE * scale); axisPaint.setTextSize(SMALL_TEXT_SIZE * scale);
@@ -320,22 +322,21 @@ public class ChartView extends View {
gridPaint = new Paint(); gridPaint = new Paint();
gridPaint.setStyle(Style.STROKE); gridPaint.setStyle(Style.STROKE);
gridPaint.setColor(context.getResources().getColor(android.R.color.darker_gray)); gridPaint.setColor(ThemeUtils.getTextColorSecondary(context));
gridPaint.setAntiAlias(false); gridPaint.setAntiAlias(false);
gridPaint.setPathEffect(new DashPathEffect(new float[]{3, 2}, 0)); gridPaint.setPathEffect(new DashPathEffect(new float[]{3, 2}, 0));
markerPaint = new Paint(); markerPaint = new Paint();
markerPaint.setStyle(Style.STROKE); markerPaint.setStyle(Style.STROKE);
markerPaint.setColor(context.getResources().getColor(android.R.color.darker_gray));
markerPaint.setAntiAlias(false); markerPaint.setAntiAlias(false);
pointer = context.getResources().getDrawable(R.drawable.ic_logo_color_24dp); pointer = context.getResources().getDrawable(R.drawable.ic_logo_color_24dp);
pointer.setBounds(0, 0, pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight()); pointer.setBounds(0, 0, pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight());
waypointMarker = MarkerUtils.getDefaultPhoto(context); markerPin = MarkerUtils.getDefaultPhoto(context);
markerWidth = waypointMarker.getIntrinsicWidth(); markerWidth = markerPin.getIntrinsicWidth();
markerHeight = waypointMarker.getIntrinsicHeight(); markerHeight = markerPin.getIntrinsicHeight();
waypointMarker.setBounds(0, 0, markerWidth, markerHeight); markerPin.setBounds(0, 0, markerWidth, markerHeight);
scroller = new Scroller(context); scroller = new Scroller(context);
setFocusable(true); setFocusable(true);
@@ -434,15 +435,15 @@ public class ChartView extends View {
scrollTo(0, 0); scrollTo(0, 0);
} }
public void addWaypoint(Marker waypoint) { public void addMarker(Marker marker) {
synchronized (waypoints) { synchronized (markers) {
waypoints.add(waypoint); markers.add(marker);
} }
} }
public void clearWaypoints() { public void clearMarker() {
synchronized (waypoints) { synchronized (markers) {
waypoints.clear(); markers.clear();
} }
} }
@@ -540,13 +541,13 @@ public class ChartView extends View {
synchronized (chartPoints) { synchronized (chartPoints) {
canvas.save(); canvas.save();
canvas.drawColor(Color.WHITE); canvas.drawColor(backgroundColor);
canvas.save(); canvas.save();
clipToGraphArea(canvas); clipToGraphArea(canvas);
drawDataSeries(canvas); drawDataSeries(canvas);
drawWaypoints(canvas); drawMarker(canvas);
drawGrid(canvas); drawGrid(canvas);
canvas.restore(); canvas.restore();
@@ -587,24 +588,19 @@ public class ChartView extends View {
} }
} }
/** private void drawMarker(Canvas canvas) {
* Draws the waypoints. synchronized (markers) {
* for (Marker marker : markers) {
* @param canvas the canvas double xValue = getMarkerXValue(marker);
*/
private void drawWaypoints(Canvas canvas) {
synchronized (waypoints) {
for (Marker waypoint : waypoints) {
double xValue = getWaypointXValue(waypoint);
if (xValue > maxX) { if (xValue > maxX) {
continue; continue;
} }
canvas.save(); canvas.save();
float x = getX(getWaypointXValue(waypoint)); float x = getX(getMarkerXValue(marker));
canvas.drawLine(x, topBorder + spacer + markerHeight / 2, x, topBorder + effectiveHeight, markerPaint); canvas.drawLine(x, topBorder + spacer + markerHeight / 2, x, topBorder + effectiveHeight, markerPaint);
canvas.translate(x - (markerWidth * WAYPOINT_X_ANCHOR), topBorder + spacer); canvas.translate(x - (markerWidth * MARKER_X_ANCHOR), topBorder + spacer);
waypointMarker.draw(canvas); markerPin.draw(canvas);
canvas.restore(); canvas.restore();
} }
} }
@@ -904,7 +900,7 @@ public class ChartView extends View {
for (ChartValueSeries chartValueSeries : seriesList) { for (ChartValueSeries chartValueSeries : seriesList) {
chartValueSeries.updateDimension(); chartValueSeries.updateDimension();
} }
float density = getContext().getResources().getDisplayMetrics().density; float density = getResources().getDisplayMetrics().density;
spacer = (int) (density * SPACER); spacer = (int) (density * SPACER);
yAxisOffset = (int) (density * Y_AXIS_OFFSET); yAxisOffset = (int) (density * Y_AXIS_OFFSET);
@@ -975,17 +971,12 @@ public class ChartView extends View {
return topBorder + yAxisOffset + (int) ((1 - percentage) * rangeHeight); return topBorder + yAxisOffset + (int) ((1 - percentage) * rangeHeight);
} }
/** private double getMarkerXValue(Marker marker) {
* Gets a waypoint's x value.
*
* @param waypoint the waypoint
*/
private double getWaypointXValue(Marker waypoint) {
if (chartByDistance) { if (chartByDistance) {
double lenghtInKm = waypoint.getLength() * UnitConversions.M_TO_KM; double length_km = marker.getLength() * UnitConversions.M_TO_KM;
return metricUnits ? lenghtInKm : lenghtInKm * UnitConversions.KM_TO_MI; return metricUnits ? length_km : length_km * UnitConversions.KM_TO_MI;
} else { } else {
return waypoint.getDuration(); return marker.getDuration();
} }
} }
@@ -69,7 +69,7 @@ public final class Marker {
} }
/** /**
* May be null if the waypoint was not loaded from the database. * May be null if the it was not loaded from the database.
*/ */
public @Nullable public @Nullable
Id getId() { Id getId() {
@@ -165,16 +165,12 @@ public final class Marker {
this.id = id; this.id = id;
} }
public Id(@NonNull String id) {
this(Long.parseLong(id));
}
//TOOD Limit visibility to TrackRecordingService / ContentProvider //TOOD Limit visibility to TrackRecordingService / ContentProvider
public long getId() { public long getId() {
return id; return id;
} }
@Deprecated //TODO Use a Waypoint.Id of null instead @Deprecated //TODO Use a Id of null instead
public boolean isValid() { public boolean isValid() {
return id != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT; return id != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
} }
@@ -22,7 +22,7 @@ import android.provider.BaseColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
/** /**
* Constants for waypoints table. * Constants for markers table.
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
@@ -36,10 +36,10 @@ public interface MarkerColumns extends BaseColumns {
String DEFAULT_SORT_ORDER = _ID; String DEFAULT_SORT_ORDER = _ID;
// Columns // Columns
String NAME = "name"; // waypoint name String NAME = "name"; // marker name
String DESCRIPTION = "description"; // waypoint description String DESCRIPTION = "description"; // marker description
String CATEGORY = "category"; // waypoint category String CATEGORY = "category"; // marker category
String ICON = "icon"; // waypoint icon String ICON = "icon"; // marker icon
String TRACKID = "trackid"; // track id String TRACKID = "trackid"; // track id
String LENGTH = "length"; // length of the track (without smoothing) String LENGTH = "length"; // length of the track (without smoothing)
@@ -118,10 +118,6 @@ public class Track {
this.id = id; this.id = id;
} }
public Id(@NonNull String id) {
this(Long.parseLong(id));
}
//TOOD Limit visibility to TrackRecordingService / ContentProvider //TOOD Limit visibility to TrackRecordingService / ContentProvider
public long getId() { public long getId() {
return id; return id;
@@ -48,6 +48,8 @@ public class TrackPoint {
this.heartRate_bpm = trackPoint.getHeartRate_bpm(); this.heartRate_bpm = trackPoint.getHeartRate_bpm();
this.cyclingCadence_rpm = trackPoint.getCyclingCadence_rpm(); this.cyclingCadence_rpm = trackPoint.getCyclingCadence_rpm();
this.power = trackPoint.getPower(); this.power = trackPoint.getPower();
this.elevationGain = trackPoint.getElevationGain();
} }
public TrackPoint(double latitude, double longitude, Double altitude, long time) { public TrackPoint(double latitude, double longitude, Double altitude, long time) {
@@ -38,7 +38,7 @@ import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns; import de.dennisguse.opentracks.content.data.TracksColumns;
/** /**
* A {@link ContentProvider} that handles access to track points, tracks, and waypoints tables. * A {@link ContentProvider} that handles access to track points, tracks, and markers tables.
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
@@ -61,9 +61,9 @@ public class CustomContentProvider extends ContentProvider {
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath(), UrlType.TRACKS.ordinal()); uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath(), UrlType.TRACKS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath() + "/*", UrlType.TRACKS_BY_ID.ordinal()); uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath() + "/*", UrlType.TRACKS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.WAYPOINTS.ordinal()); uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.MARKERS.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath() + "/#", UrlType.WAYPOINTS_BY_ID.ordinal()); uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath() + "/#", UrlType.MARKERS_BY_ID.ordinal());
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.WAYPOINTS_BY_TRACKID.ordinal()); uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.MARKERS_BY_TRACKID.ordinal());
} }
@Override @Override
@@ -100,7 +100,7 @@ public class CustomContentProvider extends ContentProvider {
table = TracksColumns.TABLE_NAME; table = TracksColumns.TABLE_NAME;
shouldVacuum = true; shouldVacuum = true;
break; break;
case WAYPOINTS: case MARKERS:
table = MarkerColumns.TABLE_NAME; table = MarkerColumns.TABLE_NAME;
break; break;
default: default:
@@ -138,10 +138,10 @@ public class CustomContentProvider extends ContentProvider {
return TracksColumns.CONTENT_TYPE; return TracksColumns.CONTENT_TYPE;
case TRACKS_BY_ID: case TRACKS_BY_ID:
return TracksColumns.CONTENT_ITEMTYPE; return TracksColumns.CONTENT_ITEMTYPE;
case WAYPOINTS: case MARKERS:
return MarkerColumns.CONTENT_TYPE; return MarkerColumns.CONTENT_TYPE;
case WAYPOINTS_BY_ID: case MARKERS_BY_ID:
case WAYPOINTS_BY_TRACKID: case MARKERS_BY_TRACKID:
return MarkerColumns.CONTENT_ITEMTYPE; return MarkerColumns.CONTENT_ITEMTYPE;
default: default:
throw new IllegalArgumentException("Unknown URL " + url); throw new IllegalArgumentException("Unknown URL " + url);
@@ -213,15 +213,15 @@ public class CustomContentProvider extends ContentProvider {
queryBuilder.setTables(TracksColumns.TABLE_NAME); queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")"); queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
break; break;
case WAYPOINTS: case MARKERS:
queryBuilder.setTables(MarkerColumns.TABLE_NAME); queryBuilder.setTables(MarkerColumns.TABLE_NAME);
sortOrder = sort != null ? sort : MarkerColumns.DEFAULT_SORT_ORDER; sortOrder = sort != null ? sort : MarkerColumns.DEFAULT_SORT_ORDER;
break; break;
case WAYPOINTS_BY_ID: case MARKERS_BY_ID:
queryBuilder.setTables(MarkerColumns.TABLE_NAME); queryBuilder.setTables(MarkerColumns.TABLE_NAME);
queryBuilder.appendWhere(MarkerColumns._ID + "=" + ContentUris.parseId(url)); queryBuilder.appendWhere(MarkerColumns._ID + "=" + ContentUris.parseId(url));
break; break;
case WAYPOINTS_BY_TRACKID: case MARKERS_BY_TRACKID:
queryBuilder.setTables(MarkerColumns.TABLE_NAME); queryBuilder.setTables(MarkerColumns.TABLE_NAME);
queryBuilder.appendWhere(MarkerColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")"); queryBuilder.appendWhere(MarkerColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
break; break;
@@ -261,11 +261,11 @@ public class CustomContentProvider extends ContentProvider {
whereClause += " AND (" + where + ")"; whereClause += " AND (" + where + ")";
} }
break; break;
case WAYPOINTS: case MARKERS:
table = MarkerColumns.TABLE_NAME; table = MarkerColumns.TABLE_NAME;
whereClause = where; whereClause = where;
break; break;
case WAYPOINTS_BY_ID: case MARKERS_BY_ID:
table = MarkerColumns.TABLE_NAME; table = MarkerColumns.TABLE_NAME;
whereClause = MarkerColumns._ID + "=" + ContentUris.parseId(url); whereClause = MarkerColumns._ID + "=" + ContentUris.parseId(url);
if (!TextUtils.isEmpty(where)) { if (!TextUtils.isEmpty(where)) {
@@ -316,8 +316,8 @@ public class CustomContentProvider extends ContentProvider {
return insertTrackPoint(url, contentValues); return insertTrackPoint(url, contentValues);
case TRACKS: case TRACKS:
return insertTrack(url, contentValues); return insertTrack(url, contentValues);
case WAYPOINTS: case MARKERS:
return insertWaypoint(url, contentValues); return insertMarker(url, contentValues);
default: default:
throw new IllegalArgumentException("Unknown url " + url); throw new IllegalArgumentException("Unknown url " + url);
} }
@@ -361,25 +361,14 @@ public class CustomContentProvider extends ContentProvider {
throw new SQLException("Failed to insert a track " + url); throw new SQLException("Failed to insert a track " + url);
} }
/** private Uri insertMarker(Uri url, ContentValues contentValues) {
* Inserts a waypoint.
*
* @param url the content url
* @param contentValues the content values
*/
private Uri insertWaypoint(Uri url, ContentValues contentValues) {
long rowId = db.insert(MarkerColumns.TABLE_NAME, MarkerColumns._ID, contentValues); long rowId = db.insert(MarkerColumns.TABLE_NAME, MarkerColumns._ID, contentValues);
if (rowId >= 0) { if (rowId >= 0) {
return ContentUris.appendId(MarkerColumns.CONTENT_URI.buildUpon(), rowId).build(); return ContentUris.appendId(MarkerColumns.CONTENT_URI.buildUpon(), rowId).build();
} }
throw new SQLException("Failed to insert a waypoint " + url); throw new SQLException("Failed to insert a marker " + url);
} }
/**
* Types of url.
*
* @author Jimmy Shih
*/
@VisibleForTesting @VisibleForTesting
enum UrlType { enum UrlType {
TRACKPOINTS, TRACKPOINTS,
@@ -387,8 +376,8 @@ public class CustomContentProvider extends ContentProvider {
TRACKPOINTS_BY_TRACKID, TRACKPOINTS_BY_TRACKID,
TRACKS, TRACKS,
TRACKS_BY_ID, TRACKS_BY_ID,
WAYPOINTS, MARKERS,
WAYPOINTS_BY_ID, MARKERS_BY_ID,
WAYPOINTS_BY_TRACKID MARKERS_BY_TRACKID
} }
} }
@@ -244,14 +244,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
@Override @Override
public void clearMarkers() { public void clearMarkers() {
if (isResumed()) { if (isResumed()) {
chartView.clearWaypoints(); chartView.clearMarker();
} }
} }
@Override @Override
public void onNewMarker(Marker marker) { public void onNewMarker(Marker marker) {
if (isResumed() && marker != null && LocationUtils.isValidLocation(marker.getLocation())) { if (isResumed() && marker != null && LocationUtils.isValidLocation(marker.getLocation())) {
chartView.addWaypoint(marker); chartView.addMarker(marker);
} }
} }
@@ -22,6 +22,8 @@ import android.view.ViewGroup;
import android.widget.BaseAdapter; import android.widget.BaseAdapter;
import android.widget.ImageView; import android.widget.ImageView;
import androidx.core.content.ContextCompat;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
@@ -76,7 +78,7 @@ class ChooseActivityTypeImageAdapter extends BaseAdapter {
} }
if (position == selected) { if (position == selected) {
imageView.setBackgroundResource(R.drawable.list_selector_background_transition_holo_dark); imageView.setBackgroundColor(ContextCompat.getColor(parent.getContext(), R.color.colorAccent));
} else { } else {
imageView.setBackgroundColor(Color.TRANSPARENT); imageView.setBackgroundColor(Color.TRANSPARENT);
} }
@@ -42,9 +42,9 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
private DeleteMarkerCaller caller; private DeleteMarkerCaller caller;
public static void showDialog(FragmentManager fragmentManager, Marker.Id... waypointIds) { public static void showDialog(FragmentManager fragmentManager, Marker.Id... markerIds) {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putParcelableArray(KEY_MARKER_IDS, waypointIds); bundle.putParcelableArray(KEY_MARKER_IDS, markerIds);
DeleteMarkerDialogFragment deleteMarkerDialogFragment = new DeleteMarkerDialogFragment(); DeleteMarkerDialogFragment deleteMarkerDialogFragment = new DeleteMarkerDialogFragment();
deleteMarkerDialogFragment.setArguments(bundle); deleteMarkerDialogFragment.setArguments(bundle);
@@ -16,12 +16,12 @@
package de.dennisguse.opentracks.fragments; package de.dennisguse.opentracks.fragments;
import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
@@ -16,12 +16,12 @@
package de.dennisguse.opentracks.fragments; package de.dennisguse.opentracks.fragments;
import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
@@ -18,8 +18,6 @@ import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import java.util.Arrays;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordingActivity; import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter; import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter;
@@ -42,17 +40,20 @@ public class IntervalsFragment extends Fragment {
private ListView intervalListView; private ListView intervalListView;
protected IntervalStatisticsAdapter.StackMode stackModeListView; protected IntervalStatisticsAdapter.StackMode stackModeListView;
private IntervalStatisticsModel.IntervalOption selectedInterval; private IntervalStatisticsModel.IntervalOption selectedInterval;
private IntervalStatisticsAdapter adapter;
private String intervalUnit;
private IntervalStatisticsAdapter adapter;
protected Spinner spinnerIntervals; protected Spinner spinnerIntervals;
protected TextView spinnerIntervalsUnit; private ArrayAdapter<IntervalStatisticsModel.IntervalOption> spinnerAdapter;
private Track.Id trackId; private Track.Id trackId;
protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> { protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) { if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
if (spinnerIntervalsUnit != null) { intervalUnit = PreferencesUtils.isMetricUnits(getContext()) ? getContext().getString(R.string.unit_kilometer) : getContext().getString(R.string.unit_mile);
spinnerIntervalsUnit.setText(PreferencesUtils.isMetricUnits(getContext()) ? getContext().getString(R.string.unit_kilometer) : getContext().getString(R.string.unit_mile)); if (adapter != null) {
adapter.notifyDataSetChanged();
spinnerAdapter.notifyDataSetChanged();
intervalChanged(); intervalChanged();
} }
} }
@@ -77,6 +78,7 @@ public class IntervalsFragment extends Fragment {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener); PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
intervalUnit = PreferencesUtils.isMetricUnits(getContext()) ? getContext().getString(R.string.unit_kilometer) : getContext().getString(R.string.unit_mile);
if (savedInstanceState != null) { if (savedInstanceState != null) {
trackId = savedInstanceState.getParcelable(TRACK_ID_KEY); trackId = savedInstanceState.getParcelable(TRACK_ID_KEY);
@@ -93,11 +95,23 @@ public class IntervalsFragment extends Fragment {
spinnerIntervals = view.findViewById(R.id.spinner_intervals); spinnerIntervals = view.findViewById(R.id.spinner_intervals);
int[] intValues = Arrays.stream(IntervalStatisticsModel.IntervalOption.values()).mapToInt(i -> i.getValue()).toArray(); spinnerAdapter = new ArrayAdapter<IntervalStatisticsModel.IntervalOption>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
TextView v = (TextView) super.getView(position, convertView, parent);
v.setText(v.getText() + " " + intervalUnit);
return v;
}
spinnerIntervals.setAdapter(new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, Arrays.stream(intValues).mapToObj(String::valueOf).toArray(String[]::new))); @Override
spinnerIntervalsUnit = view.findViewById(R.id.spinner_intervals_unit); public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
spinnerIntervalsUnit.setText(PreferencesUtils.isMetricUnits(getContext()) ? getContext().getString(R.string.unit_kilometer) : getContext().getString(R.string.unit_mile)); TextView v = (TextView) super.getDropDownView(position, convertView, parent);
v.setText(v.getText() + " " + intervalUnit);
return v;
}
};
spinnerIntervals.setAdapter(spinnerAdapter);
spinnerIntervals.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { spinnerIntervals.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
@@ -124,7 +138,6 @@ public class IntervalsFragment extends Fragment {
intervalListView = null; intervalListView = null;
adapter = null; adapter = null;
spinnerIntervals = null; spinnerIntervals = null;
spinnerIntervalsUnit = null;
viewModel = null; viewModel = null;
} }
@@ -66,8 +66,8 @@ public class MarkerDetailFragment extends Fragment {
private Handler handler; private Handler handler;
private ImageView photoView; private ImageView photoView;
private ImageView textGradient; private ImageView textGradient;
private LinearLayout waypointInfo; private LinearLayout markerInfo;
private Marker waypoint; private Marker marker;
private final Runnable hideText = new Runnable() { private final Runnable hideText = new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -85,17 +85,17 @@ public class MarkerDetailFragment extends Fragment {
@Override @Override
public void onAnimationEnd(Animation anim) { public void onAnimationEnd(Animation anim) {
textGradient.setVisibility(View.GONE); textGradient.setVisibility(View.GONE);
waypointInfo.setVisibility(View.GONE); markerInfo.setVisibility(View.GONE);
} }
}); });
textGradient.startAnimation(animation); textGradient.startAnimation(animation);
waypointInfo.startAnimation(animation); markerInfo.startAnimation(animation);
} }
}; };
public static MarkerDetailFragment newInstance(Marker.Id waypointId) { public static MarkerDetailFragment newInstance(Marker.Id markerId) {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putParcelable(KEY_MARKER_ID, waypointId); bundle.putParcelable(KEY_MARKER_ID, markerId);
MarkerDetailFragment fragment = new MarkerDetailFragment(); MarkerDetailFragment fragment = new MarkerDetailFragment();
fragment.setArguments(bundle); fragment.setArguments(bundle);
@@ -106,8 +106,8 @@ public class MarkerDetailFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Marker.Id waypointId = getArguments().getParcelable(KEY_MARKER_ID); Marker.Id markerId = getArguments().getParcelable(KEY_MARKER_ID);
if (waypointId == null) { if (markerId == null) {
Log.d(TAG, "invalid marker id"); Log.d(TAG, "invalid marker id");
getParentFragmentManager().popBackStack(); getParentFragmentManager().popBackStack();
return; return;
@@ -121,15 +121,15 @@ public class MarkerDetailFragment extends Fragment {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.marker_detail_fragment, container, false); View view = inflater.inflate(R.layout.marker_detail_fragment, container, false);
photoView = view.findViewById(R.id.marker_detail_waypoint_photo); photoView = view.findViewById(R.id.marker_detail_marker_photo);
textGradient = view.findViewById(R.id.marker_detail_waypoint_text_gradient); textGradient = view.findViewById(R.id.marker_detail_marker_text_gradient);
waypointInfo = view.findViewById(R.id.marker_detail_waypoint_info); markerInfo = view.findViewById(R.id.marker_detail_marker_info);
photoView.setOnClickListener(v -> { photoView.setOnClickListener(v -> {
handler.removeCallbacks(hideText); handler.removeCallbacks(hideText);
int visibility = waypointInfo.getVisibility() == View.GONE ? View.VISIBLE : View.GONE; int visibility = markerInfo.getVisibility() == View.GONE ? View.VISIBLE : View.GONE;
textGradient.setVisibility(visibility); textGradient.setVisibility(visibility);
waypointInfo.setVisibility(visibility); markerInfo.setVisibility(visibility);
if (visibility == View.VISIBLE) { if (visibility == View.VISIBLE) {
handler.postDelayed(hideText, HIDE_TEXT_DELAY); handler.postDelayed(hideText, HIDE_TEXT_DELAY);
} }
@@ -141,8 +141,8 @@ public class MarkerDetailFragment extends Fragment {
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
// Need to update the waypoint in case returning after an edit // Need to update the marker in case returning after an edit
updateWaypoint(true); updateMarker(true);
updateUi(); updateUi();
updateMenuItems(); updateMenuItems();
} }
@@ -171,64 +171,59 @@ public class MarkerDetailFragment extends Fragment {
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.marker_detail, menu); inflater.inflate(R.menu.marker_detail, menu);
shareMarkerImageMenuItem = menu.findItem(R.id.marker_detail_share); shareMarkerImageMenuItem = menu.findItem(R.id.marker_detail_share);
updateWaypoint(false); updateMarker(false);
updateMenuItems(); updateMenuItems();
} }
private void updateMenuItems() { private void updateMenuItems() {
if (shareMarkerImageMenuItem != null) if (shareMarkerImageMenuItem != null)
shareMarkerImageMenuItem.setVisible(waypoint.hasPhoto()); shareMarkerImageMenuItem.setVisible(marker.hasPhoto());
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
Marker.Id waypointId = getArguments().getParcelable(KEY_MARKER_ID); //TODO Should only happen in onCreate? Marker.Id markerId = getArguments().getParcelable(KEY_MARKER_ID); //TODO Should only happen in onCreate?
FragmentActivity fragmentActivity = getActivity(); FragmentActivity fragmentActivity = getActivity();
Intent intent; Intent intent;
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.marker_detail_show_on_map: case R.id.marker_detail_show_on_map:
IntentUtils.showCoordinateOnMap(getContext(), waypoint); IntentUtils.showCoordinateOnMap(getContext(), marker);
return true; return true;
case R.id.marker_detail_edit: case R.id.marker_detail_edit:
intent = IntentUtils.newIntent(fragmentActivity, MarkerEditActivity.class) intent = IntentUtils.newIntent(fragmentActivity, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, waypointId); .putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
startActivity(intent); startActivity(intent);
return true; return true;
case R.id.marker_detail_share: case R.id.marker_detail_share:
if (waypoint.hasPhoto()) { if (marker.hasPhoto()) {
intent = IntentUtils.newShareImageIntent(getContext(), waypoint.getPhotoURI()); intent = IntentUtils.newShareImageIntent(getContext(), marker.getPhotoURI());
intent = Intent.createChooser(intent, null); intent = Intent.createChooser(intent, null);
startActivity(intent); startActivity(intent);
} }
return true; return true;
case R.id.marker_detail_delete: case R.id.marker_detail_delete:
DeleteMarkerDialogFragment.showDialog(getChildFragmentManager(), waypointId); DeleteMarkerDialogFragment.showDialog(getChildFragmentManager(), markerId);
return true; return true;
default: default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
} }
/** private void updateMarker(boolean refresh) {
* Updates the waypoint. if (refresh || marker == null) {
* marker = contentProviderUtils.getMarker(getArguments().getParcelable(KEY_MARKER_ID)); //TODO Should only happen in onCreate?
* @param refresh true to always update if (marker == null) {
*/ Log.d(TAG, "marker is null");
private void updateWaypoint(boolean refresh) {
if (refresh || waypoint == null) {
waypoint = contentProviderUtils.getMarker(getArguments().getParcelable(KEY_MARKER_ID)); //TODO Should only happen in onCreate?
if (waypoint == null) {
Log.d(TAG, "waypoint is null");
getParentFragmentManager().popBackStack(); getParentFragmentManager().popBackStack();
} }
} }
} }
private void updateUi() { private void updateUi() {
boolean hasPhoto = waypoint.hasPhoto(); boolean hasPhoto = marker.hasPhoto();
if (hasPhoto) { if (hasPhoto) {
handler.removeCallbacks(hideText); handler.removeCallbacks(hideText);
photoView.setImageURI(waypoint.getPhotoURI()); photoView.setImageURI(marker.getPhotoURI());
handler.postDelayed(hideText, HIDE_TEXT_DELAY); handler.postDelayed(hideText, HIDE_TEXT_DELAY);
} else { } else {
photoView.setImageResource(MarkerUtils.ICON_ID); photoView.setImageResource(MarkerUtils.ICON_ID);
@@ -236,23 +231,23 @@ public class MarkerDetailFragment extends Fragment {
setName(hasPhoto); setName(hasPhoto);
TextView category = getView().findViewById(R.id.marker_detail_waypoint_category); TextView category = getView().findViewById(R.id.marker_detail_marker_category);
ListItemUtils.setTextView(getActivity(), category, StringUtils.getCategory(waypoint.getCategory()), hasPhoto); ListItemUtils.setTextView(getActivity(), category, StringUtils.getCategory(marker.getCategory()), hasPhoto);
TextView description = getView().findViewById(R.id.marker_detail_waypoint_description); TextView description = getView().findViewById(R.id.marker_detail_marker_description);
ListItemUtils.setTextView(getActivity(), description, waypoint.getDescription(), hasPhoto); ListItemUtils.setTextView(getActivity(), description, marker.getDescription(), hasPhoto);
setLocation(hasPhoto); setLocation(hasPhoto);
} }
private void setName(boolean addShadow) { private void setName(boolean addShadow) {
TextView textView = getView().findViewById(R.id.marker_detail_waypoint_name); TextView textView = getView().findViewById(R.id.marker_detail_marker_name);
ListItemUtils.setTextView(getActivity(), textView, waypoint.getName(), addShadow); ListItemUtils.setTextView(getActivity(), textView, marker.getName(), addShadow);
} }
private void setLocation(boolean addShadow) { private void setLocation(boolean addShadow) {
TextView textView = getView().findViewById(R.id.marker_detail_waypoint_location); TextView textView = getView().findViewById(R.id.marker_detail_marker_location);
Location location = waypoint.getLocation(); Location location = marker.getLocation();
String value; String value;
if (location == null) { if (location == null) {
value = null; value = null;
@@ -19,21 +19,19 @@ package de.dennisguse.opentracks.fragments;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.util.Pair; import android.util.Pair;
import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Spinner; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.Group; import androidx.constraintlayout.widget.Group;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackRecordedActivity;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
@@ -53,7 +51,8 @@ public class StatisticsRecordedFragment extends Fragment {
private TrackStatistics trackStatistics; private TrackStatistics trackStatistics;
private String category = ""; private String category = "";
private Track track; private Track.Id trackId;
private ContentProviderUtils contentProviderUtils;
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> { private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) { if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
@@ -76,7 +75,7 @@ public class StatisticsRecordedFragment extends Fragment {
private TextView distanceValue; private TextView distanceValue;
private TextView distanceUnit; private TextView distanceUnit;
private View activityLabel; private View activityLabel;
private Spinner activitySpinner; private ImageView activitySpinner;
private TextView movingTimeValue; private TextView movingTimeValue;
private TextView speedAvgLabel; private TextView speedAvgLabel;
private TextView speedAvgValue; private TextView speedAvgValue;
@@ -105,9 +104,8 @@ public class StatisticsRecordedFragment extends Fragment {
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Track.Id trackId = getArguments().getParcelable(TRACK_ID_KEY); trackId = getArguments().getParcelable(TRACK_ID_KEY);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getContext()); contentProviderUtils = new ContentProviderUtils(getContext());
track = contentProviderUtils.getTrack(trackId);
} }
@Override @Override
@@ -141,26 +139,6 @@ public class StatisticsRecordedFragment extends Fragment {
elevationTotalGainUnit = view.findViewById(R.id.stats_elevation_gain_unit); elevationTotalGainUnit = view.findViewById(R.id.stats_elevation_gain_unit);
} }
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Spinner activityTypeIcon = getView().findViewById(R.id.stats_activity_type_icon);
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), ""));
activityTypeIcon.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
((TrackRecordedActivity) getActivity()).chooseActivityType(category);
}
return true;
});
activityTypeIcon.setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
((TrackRecordedActivity) getActivity()).chooseActivityType(category);
}
return true;
});
}
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
@@ -206,6 +184,7 @@ public class StatisticsRecordedFragment extends Fragment {
if (isResumed()) { if (isResumed()) {
getActivity().runOnUiThread(() -> { getActivity().runOnUiThread(() -> {
if (isResumed()) { if (isResumed()) {
Track track = contentProviderUtils.getTrack(trackId);
trackStatistics = track != null ? track.getTrackStatistics() : null; trackStatistics = track != null ? track.getTrackStatistics() : null;
category = track != null ? track.getCategory() : ""; category = track != null ? track.getCategory() : "";
updateUI(); updateUI();
@@ -215,8 +194,6 @@ public class StatisticsRecordedFragment extends Fragment {
} }
private void updateUI() { private void updateUI() {
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext()); boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category); boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
@@ -231,12 +208,14 @@ public class StatisticsRecordedFragment extends Fragment {
// Set activity type // Set activity type
{ {
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
activityLabel.setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE); activityLabel.setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE);
activitySpinner.setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE); activitySpinner.setVisibility(trackIconValue != null ? View.VISIBLE : View.GONE);
activitySpinner.setEnabled(false); activitySpinner.setEnabled(false);
if (trackIconValue != null) { if (trackIconValue != null) {
TrackIconUtils.setIconSpinner(activitySpinner, trackIconValue); activitySpinner.setImageDrawable(ContextCompat.getDrawable(getContext(), TrackIconUtils.getIconDrawable(trackIconValue)));
} }
} }
@@ -201,7 +201,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
public void onActivityCreated(Bundle savedInstanceState) { public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
recordingGpsAccuracy = Integer.parseInt(getContext().getResources().getString(R.string.recording_gps_accuracy_default)); recordingGpsAccuracy = Integer.parseInt(getResources().getString(R.string.recording_gps_accuracy_default));
handlerUpdateUI = new Handler(); handlerUpdateUI = new Handler();
@@ -64,8 +64,8 @@ public class FileTrackExporter implements TrackExporter {
try { try {
trackWriter.prepare(outputStream); trackWriter.prepare(outputStream);
trackWriter.writeHeader(tracks); trackWriter.writeHeader(tracks);
for (Track track1 : tracks) { for (Track track : tracks) {
writeWaypoints(track1); writeMarkers(track);
} }
boolean hasMultipleTracks = tracks.length > 1; boolean hasMultipleTracks = tracks.length > 1;
if (hasMultipleTracks) { if (hasMultipleTracks) {
@@ -89,34 +89,31 @@ public class FileTrackExporter implements TrackExporter {
} }
} }
/** private void writeMarkers(Track track) throws InterruptedException {
* Writes the waypoints.
*/
private void writeWaypoints(Track track) throws InterruptedException {
/* /*
* TODO: Stream through the waypoints in chunks. * TODO: Stream through the markers in chunks.
* I am leaving the number of waypoints very high which should not be a problem, because we don't try to load them into objects all at the same time. * I am leaving the number of markers very high which should not be a problem, because we don't try to load them into objects all at the same time.
*/ */
boolean hasWaypoints = false; boolean hasMarkers = false;
try (Cursor cursor = contentProviderUtils.getMarkerCursor(track.getId(), null, ContentProviderUtils.MAX_LOADED_MARKERS)) { try (Cursor cursor = contentProviderUtils.getMarkerCursor(track.getId(), null, ContentProviderUtils.MAX_LOADED_MARKERS)) {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
for (int i = 0; i < cursor.getCount(); i++) { for (int i = 0; i < cursor.getCount(); i++) {
if (Thread.interrupted()) { if (Thread.interrupted()) {
throw new InterruptedException(); throw new InterruptedException();
} }
if (!hasWaypoints) { if (!hasMarkers) {
trackWriter.writeBeginWaypoints(track); trackWriter.writeBeginMarkers(track);
hasWaypoints = true; hasMarkers = true;
} }
Marker waypoint = contentProviderUtils.createMarker(cursor); Marker marker = contentProviderUtils.createMarker(cursor);
trackWriter.writeWaypoint(waypoint); trackWriter.writeMarker(marker);
cursor.moveToNext(); cursor.moveToNext();
} }
} }
} }
if (hasWaypoints) { if (hasMarkers) {
trackWriter.writeEndWaypoints(); trackWriter.writeEndMarkers();
} }
} }
@@ -33,7 +33,7 @@ import de.dennisguse.opentracks.util.StringUtils;
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
//TODO Export waypoints //TODO Export markers
public class GpxTrackWriter implements TrackWriter { public class GpxTrackWriter implements TrackWriter {
private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US); private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US);
@@ -120,27 +120,27 @@ public class GpxTrackWriter implements TrackWriter {
} }
@Override @Override
public void writeBeginWaypoints(Track track) { public void writeBeginMarkers(Track track) {
// Do nothing // Do nothing
} }
@Override @Override
public void writeEndWaypoints() { public void writeEndMarkers() {
// Do nothing // Do nothing
} }
@Override @Override
public void writeWaypoint(Marker waypoint) { public void writeMarker(Marker marker) {
if (printWriter != null) { if (printWriter != null) {
Location location = waypoint.getLocation(); Location location = marker.getLocation();
printWriter.println("<wpt " + formatLocation(location) + ">"); printWriter.println("<wpt " + formatLocation(location) + ">");
if (location.hasAltitude()) { if (location.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>"); printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
} }
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>"); printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("<name>" + StringUtils.formatCData(waypoint.getName()) + "</name>"); printWriter.println("<name>" + StringUtils.formatCData(marker.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(waypoint.getDescription()) + "</desc>"); printWriter.println("<desc>" + StringUtils.formatCData(marker.getDescription()) + "</desc>");
printWriter.println("<type>" + StringUtils.formatCData(waypoint.getCategory()) + "</type>"); printWriter.println("<type>" + StringUtils.formatCData(marker.getCategory()) + "</type>");
printWriter.println("</wpt>"); printWriter.println("</wpt>");
} }
} }
@@ -40,7 +40,7 @@ import de.dennisguse.opentracks.util.StringUtils;
*/ */
public class KmlTrackWriter implements TrackWriter { public class KmlTrackWriter implements TrackWriter {
public static final String WAYPOINT_STYLE = "waypoint"; public static final String MARKER_STYLE = "waypoint";
private static final String START_STYLE = "start"; private static final String START_STYLE = "start";
private static final String END_STYLE = "end"; private static final String END_STYLE = "end";
private static final String TRACK_STYLE = "track"; private static final String TRACK_STYLE = "track";
@@ -52,7 +52,7 @@ public class KmlTrackWriter implements TrackWriter {
public static final String EXTENDED_DATA_TYPE_POWER = "power"; public static final String EXTENDED_DATA_TYPE_POWER = "power";
public static final String EXTENDED_DATA_TYPE_ELEVATION_GAIN = "elevation_gain"; public static final String EXTENDED_DATA_TYPE_ELEVATION_GAIN = "elevation_gain";
private static final String WAYPOINT_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png"; private static final String MARKER_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
private static final String START_ICON = "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png"; private static final String START_ICON = "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png";
private static final String END_ICON = "http://maps.google.com/mapfiles/kml/paddle/red-circle.png"; private static final String END_ICON = "http://maps.google.com/mapfiles/kml/paddle/red-circle.png";
private static final String TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png"; private static final String TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
@@ -75,7 +75,7 @@ public class KmlTrackWriter implements TrackWriter {
/** /**
* @param context the context * @param context the context
* @param exportTrackDetail should detailed information about the track be exported (e.g., title, description, waypoints, timing)? * @param exportTrackDetail should detailed information about the track be exported (e.g., title, description, markers, timing)?
* @param exportSensorData should {@link TrackPoint}'s sensor data be exported? * @param exportSensorData should {@link TrackPoint}'s sensor data be exported?
* @param exportPhotos should pictures be exported (if true: exports to KMZ)? * @param exportPhotos should pictures be exported (if true: exports to KMZ)?
*/ */
@@ -124,7 +124,7 @@ public class KmlTrackWriter implements TrackWriter {
writeTrackStyle(); writeTrackStyle();
writePlacemarkerStyle(START_STYLE, START_ICON, 32, 1); writePlacemarkerStyle(START_STYLE, START_ICON, 32, 1);
writePlacemarkerStyle(END_STYLE, END_ICON, 32, 1); writePlacemarkerStyle(END_STYLE, END_ICON, 32, 1);
writePlacemarkerStyle(WAYPOINT_STYLE, WAYPOINT_ICON, 20, 2); writePlacemarkerStyle(MARKER_STYLE, MARKER_ICON, 20, 2);
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">"); printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_SPEED, context.getString(R.string.description_speed_ms)); writeSimpleArrayStyle(EXTENDED_DATA_TYPE_SPEED, context.getString(R.string.description_speed_ms));
@@ -147,7 +147,7 @@ public class KmlTrackWriter implements TrackWriter {
} }
@Override @Override
public void writeBeginWaypoints(Track track) { public void writeBeginMarkers(Track track) {
if (printWriter != null) { if (printWriter != null) {
printWriter.println("<Folder>"); printWriter.println("<Folder>");
if (exportTrackDetail) { if (exportTrackDetail) {
@@ -158,21 +158,21 @@ public class KmlTrackWriter implements TrackWriter {
} }
@Override @Override
public void writeEndWaypoints() { public void writeEndMarkers() {
if (printWriter != null) { if (printWriter != null) {
printWriter.println("</Folder>"); printWriter.println("</Folder>");
} }
} }
@Override @Override
public void writeWaypoint(Marker waypoint) { public void writeMarker(Marker marker) {
if (printWriter != null && exportTrackDetail) { if (printWriter != null && exportTrackDetail) {
boolean existsPhoto = FileUtils.getPhotoFileIfExists(context, waypoint.getTrackId(), waypoint.getPhotoURI()) != null; boolean existsPhoto = FileUtils.getPhotoFileIfExists(context, marker.getTrackId(), marker.getPhotoURI()) != null;
if (waypoint.hasPhoto() && exportPhotos && existsPhoto) { if (marker.hasPhoto() && exportPhotos && existsPhoto) {
float heading = getHeading(waypoint.getTrackId(), waypoint.getLocation()); float heading = getHeading(marker.getTrackId(), marker.getLocation());
writePhotoOverlay(waypoint, heading); writePhotoOverlay(marker, heading);
} else { } else {
writePlacemark(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), WAYPOINT_STYLE, waypoint.getLocation()); writePlacemark(marker.getName(), marker.getCategory(), marker.getDescription(), MARKER_STYLE, marker.getLocation());
} }
} }
} }
@@ -337,30 +337,24 @@ public class KmlTrackWriter implements TrackWriter {
} }
} }
/** private void writePhotoOverlay(Marker marker, float heading) {
* Writes a photo overlay from waypoint.
*
* @param waypoint Waypoint object.
* @param heading the heading.
*/
private void writePhotoOverlay(Marker waypoint, float heading) {
if (exportTrackDetail) { if (exportTrackDetail) {
printWriter.println("<PhotoOverlay>"); printWriter.println("<PhotoOverlay>");
printWriter.println("<name>" + StringUtils.formatCData(waypoint.getName()) + "</name>"); printWriter.println("<name>" + StringUtils.formatCData(marker.getName()) + "</name>");
printWriter.println("<description>" + StringUtils.formatCData(waypoint.getDescription()) + "</description>"); printWriter.println("<description>" + StringUtils.formatCData(marker.getDescription()) + "</description>");
printWriter.print("<Camera>"); printWriter.print("<Camera>");
printWriter.print("<longitude>" + waypoint.getLocation().getLongitude() + "</longitude>"); printWriter.print("<longitude>" + marker.getLocation().getLongitude() + "</longitude>");
printWriter.print("<latitude>" + waypoint.getLocation().getLatitude() + "</latitude>"); printWriter.print("<latitude>" + marker.getLocation().getLatitude() + "</latitude>");
printWriter.print("<altitude>20</altitude>"); printWriter.print("<altitude>20</altitude>");
printWriter.print("<heading>" + heading + "</heading>"); printWriter.print("<heading>" + heading + "</heading>");
printWriter.print("<tilt>90</tilt>"); printWriter.print("<tilt>90</tilt>");
printWriter.println("</Camera>"); printWriter.println("</Camera>");
printWriter.println("<TimeStamp><when>" + getTime(waypoint.getLocation()) + "</when></TimeStamp>"); printWriter.println("<TimeStamp><when>" + getTime(marker.getLocation()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + KmlTrackWriter.WAYPOINT_STYLE + "</styleUrl>"); printWriter.println("<styleUrl>#" + KmlTrackWriter.MARKER_STYLE + "</styleUrl>");
writeCategory(waypoint.getCategory()); writeCategory(marker.getCategory());
if (exportPhotos) { if (exportPhotos) {
printWriter.println("<Icon><href>" + KmzTrackExporter.buildKmzImageFilePath(waypoint) + "</href></Icon>"); printWriter.println("<Icon><href>" + KmzTrackExporter.buildKmzImageFilePath(marker) + "</href></Icon>");
} }
printWriter.print("<ViewVolume>"); printWriter.print("<ViewVolume>");
@@ -371,7 +365,7 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.print("<topFov>45</topFov>"); printWriter.print("<topFov>45</topFov>");
printWriter.println("</ViewVolume>"); printWriter.println("</ViewVolume>");
printWriter.println("<Point>"); printWriter.println("<Point>");
printWriter.println("<coordinates>" + getCoordinates(waypoint.getLocation(), ",") + "</coordinates>"); printWriter.println("<coordinates>" + getCoordinates(marker.getLocation(), ",") + "</coordinates>");
printWriter.println("</Point>"); printWriter.println("</Point>");
printWriter.println("</PhotoOverlay>"); printWriter.println("</PhotoOverlay>");
} }
@@ -105,12 +105,12 @@ public class KmzTrackExporter implements TrackExporter {
if (Thread.interrupted()) { if (Thread.interrupted()) {
throw new InterruptedException(); throw new InterruptedException();
} }
Marker waypoint = contentProviderUtils.createMarker(cursor); Marker marker = contentProviderUtils.createMarker(cursor);
if (waypoint.hasPhoto()) { if (marker.hasPhoto()) {
Uri uriPhoto = waypoint.getPhotoURI(); Uri uriPhoto = marker.getPhotoURI();
boolean existsPhoto = FileUtils.getPhotoFileIfExists(context, track.getId(), uriPhoto) != null; boolean existsPhoto = FileUtils.getPhotoFileIfExists(context, track.getId(), uriPhoto) != null;
if (existsPhoto) { if (existsPhoto) {
addImage(context, zipOutputStream, uriPhoto, waypoint); addImage(context, zipOutputStream, uriPhoto, marker);
} }
} }
@@ -121,9 +121,9 @@ public class KmzTrackExporter implements TrackExporter {
} }
} }
private void addImage(Context context, ZipOutputStream zipOutputStream, Uri uri, Marker waypoint) throws IOException { private void addImage(Context context, ZipOutputStream zipOutputStream, Uri uri, Marker marker) throws IOException {
try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) {
ZipEntry zipEntry = new ZipEntry(buildKmzImageFilePath(waypoint)); ZipEntry zipEntry = new ZipEntry(buildKmzImageFilePath(marker));
zipOutputStream.putNextEntry(zipEntry); zipOutputStream.putNextEntry(zipEntry);
if (inputStream == null) throw new FileNotFoundException(); if (inputStream == null) throw new FileNotFoundException();
@@ -146,13 +146,11 @@ public class KmzTrackExporter implements TrackExporter {
} }
/** /**
* Builds and returns the path for the image that will be saved inside KMZ_IMAGES_DIR for the waypoint. * Builds and returns the path for the image that will be saved inside KMZ_IMAGES_DIR for the marker.
*
* @param waypoint Waypoint object.
*/ */
public static String buildKmzImageFilePath(Marker waypoint) { public static String buildKmzImageFilePath(Marker marker) {
String ext = FileUtils.getExtension(waypoint.getPhotoUrl()); String ext = FileUtils.getExtension(marker.getPhotoUrl());
ext = ext == null ? "" : "." + ext; ext = ext == null ? "" : "." + ext;
return KMZ_IMAGES_DIR + File.separatorChar + FileUtils.sanitizeFileName(waypoint.getId().getId() + ext); return KMZ_IMAGES_DIR + File.separatorChar + FileUtils.sanitizeFileName(marker.getId().getId() + ext);
} }
} }
@@ -28,10 +28,10 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
* {@link #prepare(OutputStream)} * {@link #prepare(OutputStream)}
* {@link #writeHeader(Track[])} * {@link #writeHeader(Track[])}
* For each track: * For each track:
* {@link #writeBeginWaypoints(Track)} * {@link #writeBeginMarkers(Track)}
* For each waypoint: * For each marker:
* {@link #writeWaypoint(Marker)} * {@link #writeMarker(Marker)}
* {@link #writeEndWaypoints()} * {@link #writeEndMarkers()}
* {@link #writeMultiTrackBegin()} * {@link #writeMultiTrackBegin()}
* For each track: * For each track:
* {@link #writeBeginTrack(Track, TrackPoint)} * {@link #writeBeginTrack(Track, TrackPoint)}
@@ -50,11 +50,6 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
*/ */
public interface TrackWriter { public interface TrackWriter {
/**
* Prepares the output stream.
*
* @param outputStream the output stream
*/
void prepare(OutputStream outputStream); void prepare(OutputStream outputStream);
/** /**
@@ -62,77 +57,27 @@ public interface TrackWriter {
*/ */
void close(); void close();
/**
* Writes the header
*
* @param tracks the tracks
*/
void writeHeader(Track[] tracks); void writeHeader(Track[] tracks);
/**
* Writes the footer.
*/
void writeFooter(); void writeFooter();
/** void writeBeginMarkers(Track track);
* Writes the beginning of the waypoints.
*
* @param track the track
*/
void writeBeginWaypoints(Track track);
/** void writeEndMarkers();
* Writes the end of the waypoints.
*/
void writeEndWaypoints();
/** void writeMarker(Marker marker);
* Writes a waypoint.
*
* @param waypoint the waypoint
*/
void writeWaypoint(Marker waypoint);
/**
* Writes the beginning of the tracks.
*/
void writeMultiTrackBegin(); void writeMultiTrackBegin();
/**
* Writes the end of the tracks,
*/
void writeMultiTrackEnd(); void writeMultiTrackEnd();
/**
* Writes the beginning of a track.
*
* @param track the track
* @param startTrackPoint the start location
*/
void writeBeginTrack(Track track, TrackPoint startTrackPoint); void writeBeginTrack(Track track, TrackPoint startTrackPoint);
/**
* Writes the end of a track.
*
* @param track the track
* @param endTrackPoint the end location
*/
void writeEndTrack(Track track, TrackPoint endTrackPoint); void writeEndTrack(Track track, TrackPoint endTrackPoint);
/**
* Writes open segment.
*/
void writeOpenSegment(); void writeOpenSegment();
/**
* Writes close segment.
*/
void writeCloseSegment(); void writeCloseSegment();
/**
* Writes a trackPoint.
*
* @param trackPoint the trackPoint
*/
void writeTrackPoint(TrackPoint trackPoint); void writeTrackPoint(TrackPoint trackPoint);
} }
@@ -70,7 +70,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
private Track.Id importTrackId; private Track.Id importTrackId;
private final List<Track.Id> trackIds = new ArrayList<>(); private final List<Track.Id> trackIds = new ArrayList<>();
private final List<Marker> waypoints = new ArrayList<>(); private final List<Marker> markers = new ArrayList<>();
// The current element content // The current element content
//TODO Should be made private and getter be used by child classes. //TODO Should be made private and getter be used by child classes.
@@ -88,7 +88,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
protected String heartrate; protected String heartrate;
protected String cadence; protected String cadence;
protected String power; protected String power;
protected String waypointType; protected String markerType;
protected String photoUrl; protected String photoUrl;
protected String uuid; protected String uuid;
@@ -148,7 +148,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* On file end. * On file end.
*/ */
protected void onFileEnd() { protected void onFileEnd() {
// Add waypoints to the last imported track // Add markers to the last imported track
int size = trackIds.size(); int size = trackIds.size();
if (size == 0) { if (size == 0) {
return; return;
@@ -159,8 +159,8 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
return; return;
} }
int waypointPosition = -1; int markerPosition = -1;
Marker waypoint = null; Marker marker = null;
TrackPoint trackPoint = null; TrackPoint trackPoint = null;
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics().getStartTime_ms()); TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(track.getTrackStatistics().getStartTime_ms());
// TODO Should not be necessary anymore? // TODO Should not be necessary anymore?
@@ -169,18 +169,18 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false)) { try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), -1L, false)) {
while (true) { while (true) {
if (waypoint == null) { if (marker == null) {
waypointPosition++; markerPosition++;
waypoint = waypointPosition < waypoints.size() ? waypoints.get(waypointPosition) : null; marker = markerPosition < markers.size() ? markers.get(markerPosition) : null;
if (waypoint == null) { if (marker == null) {
// No more waypoints // No more markers
return; return;
} }
} }
if (trackPoint == null) { if (trackPoint == null) {
if (!trackPointIterator.hasNext()) { if (!trackPointIterator.hasNext()) {
// No more track points. Ignore the rest of the waypoints. // No more track points. Ignore the rest of the markers.
return; return;
} }
trackPoint = trackPointIterator.next(); trackPoint = trackPointIterator.next();
@@ -188,13 +188,13 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
markerTrackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval); markerTrackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
} }
if (waypoint.getLocation().getTime() > trackPoint.getTime()) { if (marker.getLocation().getTime() > trackPoint.getTime()) {
trackPoint = null; trackPoint = null;
} else if (waypoint.getLocation().getTime() < trackPoint.getTime()) { } else if (marker.getLocation().getTime() < trackPoint.getTime()) {
Log.w(TAG, "Ignoring waypoint: current trackPoint was after waypoint."); Log.w(TAG, "Ignoring marker: current trackPoint was after marker.");
waypoint = null; marker = null;
} else { } else {
// The waypoint trackPoint time matches the track point time // The marker trackPoint time matches the track point time
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) { if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
// Invalid trackPoint, load the next trackPoint // Invalid trackPoint, load the next trackPoint
trackPoint = null; trackPoint = null;
@@ -202,19 +202,19 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
} }
// Valid trackPoint // Valid trackPoint
if (trackPoint.getLatitude() == waypoint.getLocation().getLatitude() && trackPoint.getLongitude() == waypoint.getLocation().getLongitude()) { if (trackPoint.getLatitude() == marker.getLocation().getLatitude() && trackPoint.getLongitude() == marker.getLocation().getLongitude()) {
String waypointDescription = waypoint.getDescription(); String markerDescription = marker.getDescription();
String icon = context.getString(R.string.marker_waypoint_icon_url); String icon = context.getString(R.string.marker_icon_url);
double length = trackStatisticsUpdater.getTrackStatistics().getTotalDistance(); double length = trackStatisticsUpdater.getTrackStatistics().getTotalDistance();
long duration = trackStatisticsUpdater.getTrackStatistics().getTotalTime(); long duration = trackStatisticsUpdater.getTrackStatistics().getTotalTime();
// Insert waypoint // Insert marker
Marker newWaypoint = new Marker(waypoint.getName(), waypointDescription, waypoint.getCategory(), icon, track.getId(), length, duration, trackPoint.getLocation(), waypoint.getPhotoUrl()); Marker newMarker = new Marker(marker.getName(), markerDescription, marker.getCategory(), icon, track.getId(), length, duration, trackPoint.getLocation(), marker.getPhotoUrl());
contentProviderUtils.insertMarker(newWaypoint); contentProviderUtils.insertMarker(newMarker);
} }
// Load the next waypoint // Load the next marker
waypoint = null; marker = null;
} }
} }
} }
@@ -300,11 +300,8 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
trackData.lastLocationInCurrentSegment = null; trackData.lastLocationInCurrentSegment = null;
} }
/** protected void addMarker() throws SAXException {
* Adds a waypoint. // Markers must have a time, else cannot match to the track points
*/
protected void addWaypoint() throws SAXException {
// Waypoint must have a time, else cannot match to the track points
if (time == null) { if (time == null) {
return; return;
} }
@@ -314,22 +311,22 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) { if (!LocationUtils.isValidLocation(trackPoint.getLocation())) {
throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint)); throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
} }
Marker waypoint = new Marker(trackPoint.getLocation()); Marker marker = new Marker(trackPoint.getLocation());
if (name != null) { if (name != null) {
waypoint.setName(name); marker.setName(name);
} }
if (description != null) { if (description != null) {
waypoint.setDescription(description); marker.setDescription(description);
} }
if (category != null) { if (category != null) {
waypoint.setCategory(category); marker.setCategory(category);
} }
if (photoUrl != null) { if (photoUrl != null) {
waypoint.setPhotoUrl(photoUrl); marker.setPhotoUrl(photoUrl);
} }
waypoints.add(waypoint); markers.add(marker);
} }
/** /**
@@ -21,7 +21,7 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); configureDirectoryChooserIntent(intent);
startActivityForResult(intent, DIRECTORY_PICKER_REQUEST_CODE); startActivityForResult(intent, DIRECTORY_PICKER_REQUEST_CODE);
} }
@@ -33,19 +33,23 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
Uri directoryUri = resultData.getData(); Uri directoryUri = resultData.getData();
startActivity(createIntent(directoryUri)); startActivity(createNextActivityIntent(directoryUri));
} }
finish(); finish();
} }
} }
protected abstract Intent createIntent(Uri directoryUri); protected void configureDirectoryChooserIntent(Intent intent) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
protected abstract Intent createNextActivityIntent(Uri directoryUri);
public static class ImportDirectoryChooserActivity extends DirectoryChooserActivity { public static class ImportDirectoryChooserActivity extends DirectoryChooserActivity {
@Override @Override
protected Intent createIntent(Uri directoryUri) { protected Intent createNextActivityIntent(Uri directoryUri) {
Intent intent = IntentUtils.newIntent(this, ImportActivity.class); Intent intent = IntentUtils.newIntent(this, ImportActivity.class);
intent.putExtra(ImportActivity.EXTRA_DIRECTORY_URI_KEY, directoryUri); intent.putExtra(ImportActivity.EXTRA_DIRECTORY_URI_KEY, directoryUri);
return intent; return intent;
@@ -55,11 +59,17 @@ public abstract class DirectoryChooserActivity extends AppCompatActivity {
public static class ExportDirectoryChooserActivity extends DirectoryChooserActivity { public static class ExportDirectoryChooserActivity extends DirectoryChooserActivity {
@Override @Override
protected Intent createIntent(Uri directoryUri) { protected Intent createNextActivityIntent(Uri directoryUri) {
Intent intent = IntentUtils.newIntent(this, ExportActivity.class); Intent intent = IntentUtils.newIntent(this, ExportActivity.class);
intent.putExtra(ExportActivity.EXTRA_DIRECTORY_URI_KEY, directoryUri); intent.putExtra(ExportActivity.EXTRA_DIRECTORY_URI_KEY, directoryUri);
intent.putExtra(ExportActivity.EXTRA_TRACKFILEFORMAT_KEY, PreferencesUtils.getExportTrackFileFormat(this)); intent.putExtra(ExportActivity.EXTRA_TRACKFILEFORMAT_KEY, PreferencesUtils.getExportTrackFileFormat(this));
return intent; return intent;
} }
@Override
protected void configureDirectoryChooserIntent(Intent intent) {
super.configureDirectoryChooserIntent(intent);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
} }
} }
@@ -44,7 +44,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG_TRACK_POINT = "trkpt"; private static final String TAG_TRACK_POINT = "trkpt";
private static final String TAG_TRACK_SEGMENT = "trkseg"; private static final String TAG_TRACK_SEGMENT = "trkseg";
private static final String TAG_TYPE = "type"; private static final String TAG_TYPE = "type";
private static final String TAG_WAYPOINT = "wpt"; private static final String TAG_MARKER = "wpt";
private static final String TAG_ID = "opentracks:trackid"; private static final String TAG_ID = "opentracks:trackid";
private static final String ATTRIBUTE_LAT = "lat"; private static final String ATTRIBUTE_LAT = "lat";
@@ -71,8 +71,8 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
@Override @Override
public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException { public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException {
switch (tag) { switch (tag) {
case TAG_WAYPOINT: case TAG_MARKER:
onWaypointStart(attributes); onMarkerStart(attributes);
break; break;
case TAG_TRACK: case TAG_TRACK:
onTrackStart(); onTrackStart();
@@ -92,8 +92,8 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
case TAG_GPX: case TAG_GPX:
onFileEnd(); onFileEnd();
break; break;
case TAG_WAYPOINT: case TAG_MARKER:
onWaypointEnd(); onMarkerEnd();
break; break;
case TAG_TRACK: case TAG_TRACK:
onTrackEnd(); onTrackEnd();
@@ -128,7 +128,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
break; break;
case TAG_COMMENT: case TAG_COMMENT:
if (content != null) { if (content != null) {
waypointType = content.trim(); markerType = content.trim();
} }
break; break;
case TAG_EXTENSION_SPEED: case TAG_EXTENSION_SPEED:
@@ -187,12 +187,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
insertTrackPoint(trackPoint); insertTrackPoint(trackPoint);
} }
/** private void onMarkerStart(Attributes attributes) {
* On waypoint start.
*
* @param attributes the attributes
*/
private void onWaypointStart(Attributes attributes) {
name = null; name = null;
description = null; description = null;
category = null; category = null;
@@ -201,13 +196,10 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
longitude = attributes.getValue(ATTRIBUTE_LON); longitude = attributes.getValue(ATTRIBUTE_LON);
altitude = null; altitude = null;
time = null; time = null;
waypointType = null; markerType = null;
} }
/** private void onMarkerEnd() throws SAXException {
* On waypoint end. addMarker();
*/
private void onWaypointEnd() throws SAXException {
addWaypoint();
} }
} }
@@ -39,7 +39,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG = KmlFileTrackImporter.class.getSimpleName(); private static final String TAG = KmlFileTrackImporter.class.getSimpleName();
private static final String WAYPOINT_STYLE = "#" + KmlTrackWriter.WAYPOINT_STYLE; private static final String MARKER_STYLE = "#" + KmlTrackWriter.MARKER_STYLE;
private static final String TAG_COORDINATES = "coordinates"; private static final String TAG_COORDINATES = "coordinates";
private static final String TAG_DESCRIPTION = "description"; private static final String TAG_DESCRIPTION = "description";
@@ -84,8 +84,8 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
switch (tag) { switch (tag) {
case TAG_PLACEMARK: case TAG_PLACEMARK:
case TAG_PHOTO_OVERLAY: case TAG_PHOTO_OVERLAY:
// Note that a track is contained in a Placemark, calling onWaypointStart will clear various track variables like name, category, and description. // Note that a track is contained in a Placemark, calling onMarkerStart will clear various track variables like name, category, and description.
onWaypointStart(); onMarkerStart();
break; break;
case TAG_GX_MULTI_TRACK: case TAG_GX_MULTI_TRACK:
trackStarted = true; trackStarted = true;
@@ -111,11 +111,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
break; break;
case TAG_PLACEMARK: case TAG_PLACEMARK:
case TAG_PHOTO_OVERLAY: case TAG_PHOTO_OVERLAY:
// Note that a track is contained in a Placemark, calling onWaypointend is save since waypointType is not set for a track. // Note that a track is contained in a Placemark, calling onMarkerEnd is save since markerType is not set for a track.
onWaypointEnd(); onMarkerEnd();
break; break;
case TAG_COORDINATES: case TAG_COORDINATES:
onWaypointLocationEnd(); onMarkerLocationEnd();
break; break;
case TAG_GX_MULTI_TRACK: case TAG_GX_MULTI_TRACK:
onTrackEnd(); onTrackEnd();
@@ -161,7 +161,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
break; break;
case TAG_STYLE_URL: case TAG_STYLE_URL:
if (content != null) { if (content != null) {
waypointType = content.trim(); markerType = content.trim();
} }
break; break;
case TAG_HREF: case TAG_HREF:
@@ -175,10 +175,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
content = null; content = null;
} }
/** private void onMarkerStart() {
* On waypoint start.
*/
private void onWaypointStart() {
// Reset all Placemark variables // Reset all Placemark variables
name = null; name = null;
icon = null; icon = null;
@@ -189,27 +186,21 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
longitude = null; longitude = null;
altitude = null; altitude = null;
time = null; time = null;
waypointType = null; markerType = null;
} }
/** private void onMarkerEnd() throws SAXException {
* On waypoint end. if (!MARKER_STYLE.equals(markerType)) {
*/
private void onWaypointEnd() throws SAXException {
if (!WAYPOINT_STYLE.equals(waypointType)) {
return; return;
} }
// If there is photoUrl it has to be changed because that url in kml file is a relative path to the internal kmz file. // If there is photoUrl it has to be changed because that url in kml file is a relative path to the internal kmz file.
photoUrl = getInternalPhotoUrl(photoUrl); photoUrl = getInternalPhotoUrl(photoUrl);
addWaypoint(); addMarker();
} }
/** private void onMarkerLocationEnd() {
* On waypoint location end.
*/
private void onWaypointLocationEnd() {
if (content != null) { if (content != null) {
String[] parts = content.trim().split(","); String[] parts = content.trim().split(",");
if (parts.length != 2 && parts.length != 3) { if (parts.length != 2 && parts.length != 3) {
@@ -191,20 +191,20 @@ public class KmzTrackImporter implements TrackImporter {
} }
/** /**
* Deletes all images that remained in external storage that doesn't have waypoint (marker) associated. * Deletes all images that remained in external storage that doesn't have a marker associated.
* *
* @param context the Context object. * @param context the Context object.
* @param trackId the id of the Track. * @param trackId the id of the Track.
*/ */
private void deleteOrphanImages(Context context, Track.Id trackId) { private void deleteOrphanImages(Context context, Track.Id trackId) {
if (!trackId.isValid()) { if (!trackId.isValid()) {
// 1.- Gets all photo names in the waypoints of the track identified by id. // 1.- Gets all photo names in the markers of the track identified by id.
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context); ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
List<Marker> waypoints = contentProviderUtils.getMarkers(trackId); List<Marker> markers = contentProviderUtils.getMarkers(trackId);
List<String> photosName = new ArrayList<>(); List<String> photosName = new ArrayList<>();
for (Marker w : waypoints) { for (Marker marker : markers) {
if (w.hasPhoto()) { if (marker.hasPhoto()) {
String photoUrl = Uri.decode(w.getPhotoUrl()); String photoUrl = Uri.decode(marker.getPhotoUrl());
photosName.add(photoUrl.substring(photoUrl.lastIndexOf(File.separatorChar) + 1)); photosName.add(photoUrl.substring(photoUrl.lastIndexOf(File.separatorChar) + 1));
} }
} }
@@ -254,7 +254,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
category = category != null ? category : ""; category = category != null ? category : "";
description = description != null ? description : ""; description = description != null ? description : "";
String icon = getString(R.string.marker_waypoint_icon_url); String icon = getString(R.string.marker_icon_url);
photoUrl = photoUrl != null ? photoUrl : ""; photoUrl = photoUrl != null ? photoUrl : "";
TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics(); TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
@@ -102,7 +102,7 @@ class GpsStatus {
* It look for GPS changes in lastLocation if it's not null. If it's null then look for in lastValidLocation if any. * It look for GPS changes in lastLocation if it's not null. If it's null then look for in lastValidLocation if any.
*/ */
public void onLocationChanged(final Location location) { public void onLocationChanged(final Location location) {
if (checking == true) { if (checking) {
return; return;
} }
@@ -169,9 +169,8 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(track.getId()); List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(track.getId());
IntervalStatistics intervalStatistics = new IntervalStatistics(); IntervalStatistics intervalStatistics = new IntervalStatistics();
intervalStatistics.build(trackPoints, (float) (PreferencesUtils.isMetricUnits(context) ? 1 * UnitConversions.KM_TO_M : 1 * UnitConversions.MI_TO_M)); intervalStatistics.build(trackPoints, (float) (PreferencesUtils.isMetricUnits(context) ? 1d * UnitConversions.KM_TO_M : 1d * UnitConversions.MI_TO_M));
int numIntervals = intervalStatistics.getIntervalList().size(); IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
IntervalStatistics.Interval lastInterval = numIntervals > 0 ? intervalStatistics.getIntervalList().get(numIntervals - 1) : null;
String announcement = AnnouncementUtils.getAnnouncement(context, trackStatistics, category, lastInterval); String announcement = AnnouncementUtils.getAnnouncement(context, trackStatistics, category, lastInterval);
speakAnnouncement(announcement); speakAnnouncement(announcement);
@@ -21,6 +21,7 @@ import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference; import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeCyclingCadenceAndSpeedPreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference; import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeHeartRatePreference;
import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeSensorPreference; import de.dennisguse.opentracks.settings.bluetooth.BluetoothLeSensorPreference;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.BluetoothUtils; import de.dennisguse.opentracks.util.BluetoothUtils;
import de.dennisguse.opentracks.util.HackUtils; import de.dennisguse.opentracks.util.HackUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -65,6 +66,9 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
if (PreferencesUtils.isKey(getActivity(), R.string.stats_units_key, key)) { if (PreferencesUtils.isKey(getActivity(), R.string.stats_units_key, key)) {
getActivity().runOnUiThread(this::updateUnits); getActivity().runOnUiThread(this::updateUnits);
} }
if (PreferencesUtils.isKey(getActivity(), R.string.night_mode_key, key)) {
getActivity().runOnUiThread(() -> ActivityUtils.applyNightMode(getContext()));
}
}; };
// Used to forward update from ChooseActivityTypeDialogFragment; TODO Could be replaced with LiveData. // Used to forward update from ChooseActivityTypeDialogFragment; TODO Could be replaced with LiveData.
@@ -16,6 +16,7 @@ import android.widget.AbsListView;
import android.widget.ListView; import android.widget.ListView;
import android.widget.Toast; import android.widget.Toast;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.SearchView;
import java.util.ArrayList; import java.util.ArrayList;
@@ -151,4 +152,7 @@ public class ActivityUtils {
toast.show(); toast.show();
} }
public static void applyNightMode(Context context) {
AppCompatDelegate.setDefaultNightMode(PreferencesUtils.getDefaultNightMode(context));
}
} }
@@ -31,7 +31,7 @@ public class AnnouncementUtils {
String rate; String rate;
String currentRate; String currentRate;
String currentRateMsg = ""; String currentRateMsg;
if (reportSpeed) { if (reportSpeed) {
int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour; int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour;
rate = context.getResources().getQuantityString(speedId, getQuantityCount(distancePerTime), distancePerTime); rate = context.getResources().getQuantityString(speedId, getQuantityCount(distancePerTime), distancePerTime);
@@ -16,12 +16,13 @@
package de.dennisguse.opentracks.util; package de.dennisguse.opentracks.util;
import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import androidx.appcompat.app.AlertDialog;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
/** /**
@@ -45,7 +46,7 @@ public class DialogUtils {
public static Dialog createConfirmationDialog(final Context context, int titleId, String message, DialogInterface.OnClickListener okListener) { public static Dialog createConfirmationDialog(final Context context, int titleId, String message, DialogInterface.OnClickListener okListener) {
return new AlertDialog.Builder(context) return new AlertDialog.Builder(context)
.setCancelable(true) .setCancelable(true)
.setIcon(android.R.drawable.ic_dialog_alert) .setIcon(R.drawable.ic_delete_forever_24dp)
.setMessage(message) .setMessage(message)
.setNegativeButton(R.string.generic_no, null) .setNegativeButton(R.string.generic_no, null)
.setPositiveButton(R.string.generic_yes, okListener) .setPositiveButton(R.string.generic_yes, okListener)
@@ -34,7 +34,7 @@ public class IntentDashboardUtils {
private static final int TRACK_URI_INDEX = 0; private static final int TRACK_URI_INDEX = 0;
private static final int TRACKPOINTS_URI_INDEX = 1; private static final int TRACKPOINTS_URI_INDEX = 1;
private static final int WAYPOINTS_URI_INDEX = 2; private static final int MARKERS_URI_INDEX = 2;
private IntentDashboardUtils() { private IntentDashboardUtils() {
} }
@@ -55,7 +55,7 @@ public class IntentDashboardUtils {
ArrayList<Uri> uris = new ArrayList<>(); ArrayList<Uri> uris = new ArrayList<>();
uris.add(TRACK_URI_INDEX, Uri.withAppendedPath(TracksColumns.CONTENT_URI, trackIdList)); uris.add(TRACK_URI_INDEX, Uri.withAppendedPath(TracksColumns.CONTENT_URI, trackIdList));
uris.add(TRACKPOINTS_URI_INDEX, Uri.withAppendedPath(TrackPointsColumns.CONTENT_URI_BY_TRACKID, trackIdList)); uris.add(TRACKPOINTS_URI_INDEX, Uri.withAppendedPath(TrackPointsColumns.CONTENT_URI_BY_TRACKID, trackIdList));
uris.add(WAYPOINTS_URI_INDEX, Uri.withAppendedPath(MarkerColumns.CONTENT_URI_BY_TRACKID, trackIdList)); uris.add(MARKERS_URI_INDEX, Uri.withAppendedPath(MarkerColumns.CONTENT_URI_BY_TRACKID, trackIdList));
Intent intent = new Intent(ACTION_DASHBOARD); Intent intent = new Intent(ACTION_DASHBOARD);
intent.putParcelableArrayListExtra(ACTION_DASHBOARD_PAYLOAD, uris); intent.putParcelableArrayListExtra(ACTION_DASHBOARD_PAYLOAD, uris);
@@ -70,7 +70,7 @@ public class IntentDashboardUtils {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
ClipData clipData = ClipData.newRawUri(null, uris.get(TRACK_URI_INDEX)); ClipData clipData = ClipData.newRawUri(null, uris.get(TRACK_URI_INDEX));
clipData.addItem(new ClipData.Item(uris.get(TRACKPOINTS_URI_INDEX))); clipData.addItem(new ClipData.Item(uris.get(TRACKPOINTS_URI_INDEX)));
clipData.addItem(new ClipData.Item(uris.get(WAYPOINTS_URI_INDEX))); clipData.addItem(new ClipData.Item(uris.get(MARKERS_URI_INDEX)));
intent.setClipData(clipData); intent.setClipData(clipData);
context.startActivity(intent); context.startActivity(intent);
@@ -105,7 +105,7 @@ public class IntentUtils {
} }
/** /**
* Creates an intent to share a waypoint image with an app. * Creates an intent to share an image with an app.
* *
* @param context the context. * @param context the context.
* @param uri uri with the image to share. * @param uri uri with the image to share.
@@ -128,8 +128,8 @@ public class IntentUtils {
.putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_image_body)); .putExtra(Intent.EXTRA_TEXT, context.getString(R.string.share_image_body));
} }
public static void showCoordinateOnMap(Context context, Marker waypoint) { public static void showCoordinateOnMap(Context context, Marker marker) {
showCoordinateOnMap(context, waypoint.getLocation().getLatitude(), waypoint.getLocation().getLongitude(), waypoint.getName()); showCoordinateOnMap(context, marker.getLocation().getLatitude(), marker.getLocation().getLongitude(), marker.getName());
} }
/** /**
@@ -108,6 +108,8 @@ public class PreferencesUtils {
* @param keyId the key id * @param keyId the key id
* @param value the value * @param value the value
*/ */
//TODO Don't use; this function is only to be used TrackRecordingService and will be removed.
@VisibleForTesting
public static void setBoolean(Context context, int keyId, boolean value) { public static void setBoolean(Context context, int keyId, boolean value) {
SharedPreferences sharedPreferences = getSharedPreferences(context); SharedPreferences sharedPreferences = getSharedPreferences(context);
Editor editor = sharedPreferences.edit(); Editor editor = sharedPreferences.edit();
@@ -162,6 +164,8 @@ public class PreferencesUtils {
* @param keyId the key id * @param keyId the key id
* @param value the value * @param value the value
*/ */
//TODO Don't use; this function is only to be used TrackRecordingService and will be removed.
@Deprecated
@VisibleForTesting @VisibleForTesting
public static void setLong(Context context, int keyId, long value) { public static void setLong(Context context, int keyId, long value) {
SharedPreferences sharedPreferences = getSharedPreferences(context); SharedPreferences sharedPreferences = getSharedPreferences(context);
@@ -189,6 +193,7 @@ public class PreferencesUtils {
* @param keyId the key id * @param keyId the key id
* @param value the value * @param value the value
*/ */
@VisibleForTesting
public static void setString(Context context, int keyId, String value) { public static void setString(Context context, int keyId, String value) {
SharedPreferences sharedPreferences = getSharedPreferences(context); SharedPreferences sharedPreferences = getSharedPreferences(context);
Editor editor = sharedPreferences.edit(); Editor editor = sharedPreferences.edit();
@@ -347,6 +352,16 @@ public class PreferencesUtils {
return getBoolean(context, R.string.import_prevent_reimport_key, defaultValue); return getBoolean(context, R.string.import_prevent_reimport_key, defaultValue);
} }
/**
* @return {@link androidx.appcompat.app.AppCompatDelegate}.MODE_*
*/
public static int getDefaultNightMode(Context context) {
final String defaultValue = getKey(context, R.string.night_mode_default);
final String value = getString(context, R.string.night_mode_key, defaultValue);
return Integer.parseInt(value);
}
@Deprecated //Use TrackRecordingService @Deprecated //Use TrackRecordingService
public static boolean isRecording(Context context) { public static boolean isRecording(Context context) {
return isRecording(getRecordingTrackId(context)); return isRecording(getRecordingTrackId(context));
@@ -0,0 +1,40 @@
package de.dennisguse.opentracks.util;
import android.content.Context;
import android.content.res.Resources;
import android.util.TypedValue;
import androidx.core.content.ContextCompat;
public class ThemeUtils {
private ThemeUtils() {
}
/**
* Get the material design default background color.
*/
public static int getBackgroundColor(Context context) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
theme.resolveAttribute(android.R.attr.colorBackground, typedValue, true);
return ContextCompat.getColor(context, typedValue.resourceId);
}
public static int getTextColorPrimary(Context context) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true);
return ContextCompat.getColor(context, typedValue.resourceId);
}
public static int getTextColorSecondary(Context context) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
theme.resolveAttribute(android.R.attr.textColorSecondary, typedValue, true);
return ContextCompat.getColor(context, typedValue.resourceId);
}
}
@@ -55,7 +55,7 @@ public class TrackIconUtils {
private static final String SKATE_BOARDING = "SKATE_BOARDING"; private static final String SKATE_BOARDING = "SKATE_BOARDING";
private static final String CLIMBING = "CLIMBING"; private static final String CLIMBING = "CLIMBING";
private static final int ACTIVITY_UNKNOWN_LOGO = R.drawable.ic_logo_white_24dp; private static final int ACTIVITY_UNKNOWN_LOGO = R.drawable.ic_logo_24dp;
private static final int[] AIRPLANE_LIST = new int[]{R.string.activity_type_airplane, R.string.activity_type_commercial_airplane, R.string.activity_type_rc_airplane}; private static final int[] AIRPLANE_LIST = new int[]{R.string.activity_type_airplane, R.string.activity_type_commercial_airplane, R.string.activity_type_rc_airplane};
private static final int[] BIKE_LIST = new int[]{R.string.activity_type_biking, R.string.activity_type_cycling, R.string.activity_type_dirt_bike, R.string.activity_type_road_biking, R.string.activity_type_track_cycling}; private static final int[] BIKE_LIST = new int[]{R.string.activity_type_biking, R.string.activity_type_cycling, R.string.activity_type_dirt_bike, R.string.activity_type_road_biking, R.string.activity_type_track_cycling};
@@ -154,7 +154,8 @@ public class TrackIconUtils {
* @param context the context * @param context the context
* @param activityType the activity type * @param activityType the activity type
*/ */
public static String getIconValue(Context context, String activityType) { public static @NonNull
String getIconValue(Context context, String activityType) {
if (activityType == null || activityType.equals("")) { if (activityType == null || activityType.equals("")) {
return UNKNOWN; return UNKNOWN;
} }
@@ -35,17 +35,22 @@ public class TrackUtils {
} }
public static void updateTrack(Context context, Track track, String name, String category, String description, ContentProviderUtils contentProviderUtils) { public static void updateTrack(Context context, Track track, String name, String category, String description, ContentProviderUtils contentProviderUtils) {
boolean update = false;
if (name != null) { if (name != null) {
track.setName(name); track.setName(name);
update = true;
} }
if (category != null) { if (category != null) {
track.setCategory(category); track.setCategory(category);
track.setIcon(TrackIconUtils.getIconValue(context, category)); track.setIcon(TrackIconUtils.getIconValue(context, category));
update = true;
} }
if (description != null) { if (description != null) {
track.setDescription(description); track.setDescription(description);
update = true;
} }
if (update) {
contentProviderUtils.updateTrack(track); contentProviderUtils.updateTrack(track);
} }
}
} }
@@ -8,7 +8,8 @@ import de.dennisguse.opentracks.util.LocationUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
public class IntervalStatistics { public class IntervalStatistics {
List<Interval> intervalList = new ArrayList<>(); private List<Interval> intervalList = new ArrayList<>();
private float distanceInterval_m;
/** /**
* @param trackPointList the list of TrackPoint. * @param trackPointList the list of TrackPoint.
@@ -16,6 +17,7 @@ public class IntervalStatistics {
*/ */
public void build(List<TrackPoint> trackPointList, float distanceInterval_m) { public void build(List<TrackPoint> trackPointList, float distanceInterval_m) {
intervalList.clear(); intervalList.clear();
this.distanceInterval_m = distanceInterval_m;
if (trackPointList == null || trackPointList.size() == 0) { if (trackPointList == null || trackPointList.size() == 0) {
return; return;
@@ -51,6 +53,30 @@ public class IntervalStatistics {
return intervalList; return intervalList;
} }
/**
* Return the last completed interval.
* An interval is complete if its distance is equal to distanceInterval_m.
*
* @return the interval object or null if any interval is completed.
*/
public Interval getLastInterval() {
if (intervalList == null) {
return null;
}
if (intervalList.size() == 1 && intervalList.get(0).getDistance_m() < distanceInterval_m) {
return null;
}
for (int i = intervalList.size() - 1; i >= 0; i--) {
if (intervalList.get(i).getDistance_m() >= distanceInterval_m) {
return this.intervalList.get(i);
}
}
return null;
}
public static class Interval { public static class Interval {
private float distance_m = 0f; private float distance_m = 0f;
private float time_ms = 0f; private float time_ms = 0f;
@@ -75,5 +75,10 @@ public class IntervalStatisticsModel extends AndroidViewModel {
public int getValue() { public int getValue() {
return value; return value;
} }
@Override
public String toString() {
return "" + value;
}
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 761 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 475 B

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