Compare commits

..

1 Commits

Author SHA1 Message Date
Román 16c20287ca Release v3.9.3 2020-08-31 12:45:20 +02:00
444 changed files with 8879 additions and 14390 deletions
-8
View File
@@ -1,13 +1,5 @@
# Thanks for your contribution.
## PLEASE REMOVE
To support us in providing a nice (and fast) open-source experience:
1. Verify that the tests are passing
2. Check that the code is properly formatted (using AndroidStudio's autoformatter)
3. Provide write access to the [branch](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork)
4. If the PR is not ready for review, please submit it as a [draft](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests)
## PLEASE REMOVE
**Describe the pull request**
A clear and concise description of what the pull request changes/adds.
+3
View File
@@ -19,6 +19,9 @@ jobs:
with:
fetch-depth: 2
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
-21
View File
@@ -1,26 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
</JetCodeStyleSettings>
<MarkdownNavigatorCodeStyleSettings>
<option name="RIGHT_MARGIN" value="72" />
</MarkdownNavigatorCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
-35
View File
@@ -1,35 +0,0 @@
# How to contribute?
## Testing
Just use OpenTracks and tell us what can be improved by opening an issue [here](https://github.com/OpenTracksApp/OpenTracks/issues/new/choose)
If we broke things (happens now and then), please tell us also.
## Coding new features
You have a cool idea for a feature that is missing in OpenTracks?
And you would like to help by implementing it?
If not, you can just take a look at the [issue tracker](https://github.com/OpenTracksApp/OpenTracks/issues).
Advices:
* creating an issue beforehand might be a good idea
Sometimes it is helpful to discuss your idea and also your implementation approach first.
We may be able to give you some hints on how to tackle the problem.
It also makes merging your changes easier and, usually, faster.
* please only add new dependencies if is absolutely necessary
If you need to:
* tell us why those are required,
* what alternatives are existing, and
* why you choose this dependency.
And also verify that the license(s) are compatible with _Apache 2.0_ as well as open-source.
## Coding conventions
None so far.
Just keep it simple.
And follow the _scout rule_.
You can take a look at existing code and get a feeling for it.
Please autoformat your code using AndroidStudio's code formatter.
-201
View File
@@ -1,201 +0,0 @@
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
@@ -0,0 +1,194 @@
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.
+28 -51
View File
@@ -2,100 +2,77 @@
_OpenTracks_ is a sport tracking application that completely respects your privacy.
[![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech)
<table>
<tr>
<th>Free</th>
<th>Paid (same as free)</th>
<th>Donations</th>
<th>Translations</th>
</tr>
<tr>
<td align="center">
<td>
<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">
</a>
</td>
<td align="center">
<a href="https://liberapay.com/OpenTracks/donate">
<img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" height="45" align="middle">
</a>
</td>
<td align="center">
<a href="https://weblate.bubu1.eu/engage/OpenTracks/">
<img src="https://weblate.bubu1.eu/widgets/OpenTracks/-/opentracks/svg-badge.svg" alt="Translation status" />
</a>
</td>
</tr>
<tr>
<td align="center">
<img alt="OpenTracks version published on F-Droid" src="https://img.shields.io/f-droid/v/de.dennisguse.opentracks.svg" align="middle" >
</td>
<td align="center">
<a href="bitcoin:bc1qckpls3lpjlcpyxzm66hcy65hnesas9zgacy6yc">
<img alt="bitcoin:bc1qckpls3lpjlcpyxzm66hcy65hnesas9zgacy6yc" src="promotion/donations-bitcoin.png" height="90" align="middle">
</a>
</td align="center">
<td></td>
</tr>
<tr>
<td></td>
<td align="center">
<td>
<a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" height="60" align="middle">
</a>
</td>
<td></td>
<td>
<a href="https://liberapay.com/OpenTracks/donate">
<img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" height="45" align="middle">
</a>
</td>
<td>
<a href="https://www.transifex.com/opentracks/opentracks/">
Transifex
</a>
</td>
</tr>
</table>
## Features:
* __Tracking:__ track your sport and outdoor activities
* __Tracking:__ track your sport and outdoor activities
* __Photos and Markers:__ mark interesting locations while tracking (also with photos)
* __Bluetooth LE sensors:__
* __Bluetooth LE sensors:__
* heart rate
* cycling: speed and cadence
* cycling: power meter
* __Export data:__ export tracks either as KMZ (incl. photos), KML or GPX
* __No Internet access:__ Internet is not used
* __Voice announcements__
* __No advertising__
* __Privacy:__
* _does not contain any in-app analytics_
* _does not use Google Play Services_
* _does not use Google Play Services_
* _has no cloud integration_
__Only required permissions:__
* _ACCESS_FINE_LOCATION_: required to use the GPS.
An overview of Bluetooth LE sensors that are known to work with OpenTracks is in [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md).
_Please note:_
_OpenTracks_ does not contain a _map_ (as this would require internet access).
_OpenTracks_ does not contain a _map_ (as this would required Internet access).
## Custom Dashboards (incl. Map)
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 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)).
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)).
However, this is rather slow and does not provide updates while recording.
## Screenshots
<div>
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_1.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_2.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_3.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_4.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_5.png">
</div>
<div>
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_6.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_7.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_8.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_9.png">
<img width="18%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_10.png">
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_1.png">
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_2.png">
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_3.png">
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_4.png">
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_5.png">
<img width="15%" src="fastlane/metadata/android/en-US/phoneScreenshots/screenshot_6.png">
</div>
## Project history
@@ -108,8 +85,8 @@ Plonk42's version is available [here](https://github.com/Plonk42/mytracks).
In 2019, _OpenTracks_ was forked from Plonk42's _My Tracks_ and major rework was conducted.
Rework of _OpenTracks_ included:
* removing Google's analytics code,
* removing integration into Google Drive,
* removing Google's analytics code,
* removing integration into Google Drive,
* removing Google Maps integration,
* removing Google Earth integration,
* removing use of Google Play service,
@@ -119,7 +96,7 @@ Rework of _OpenTracks_ included:
* removing Protobuf (store sensosensorr data in SQLite columns directly), and
* removing Android Service API for other apps.
Artwork, logos and user interface remained more or less unchanged.
Artwork, logos and user interface remained more or less unchanged.
More information about _Google My Tracks_:
* [Wikipedia page](https://en.wikipedia.org/wiki/MyTracks)
+1 -1
View File
@@ -34,4 +34,4 @@ Requirement: `gem install fastlane`
## Translations (localization)
Translations of the OpenTracks are handled in Weblate: [https://weblate.bubu1.eu/projects/OpenTracks/](https://weblate.bubu1.eu/projects/OpenTracks/).
Translations of the OpenTracks are handled in Transifex: [https://www.transifex.com/opentracks](https://www.transifex.com/opentracks/).
+2 -26
View File
@@ -1,44 +1,20 @@
# Overview of tested Bluetooth LE sensors
## HRS: Heartrate Service
* Decathlon|Geonaute|Kalenji Dual HRM Belt
* moofit heart rate monitor
* Polar H7
* Polar H10
* Polar OH1
Has updatable firmware that requires a user account at polar.com
* Wahoo Tickr (Model: WFBTHR02)
* Mi Band 3, Amazfit Band 5, Amazfit Bip. These devices are manufactured by Huami, and other Huami devices will very likely work. Theoretically, this sensor must be enabled, but testing reveals that it works even without enabling. For details on how to enable see [Gadgetbridge wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Huami-Heartrate-measurement#bluetooth-heart-rate-sensor)
## CSCP: Cycling Cadence and Speed Service
Please note that the according to the specification these sensors _may_ provide speed as well as cadence.
However, often only one value is provided.
### Speed
* Garmin Speed Sensor 2
* Wahoo Speed (Model: WFRPMSPD)
### Cadence
* Garmin Cadence Sensor 2
### Cadence
* Wahoo Cadence (Model: WFPODCAD2)
This sensor reports cadence data as speed.
A workaround is in place.
## Cycling Power Measurement
We only support _Instantaneous Power_.
_WARNING_: we only support physically movements with valid GPS coordinates.
Using a device indoors (no GPS available) will only show the current power values, but won't store anything.
Also the distance is not computed.
* Assioma Duo Powermeter
* Elite drivo II
* QUARQ Red DZero Powermeter
* Tacx Satori Smart
* Wahoo Kickr v4.0
A workaround is in place.
-51
View File
@@ -1,51 +0,0 @@
#!/bin/bash
set -e
CHANGELOG_DIR="fastlane/metadata/android/en-US/changelogs/"
echo "Checking main branch"
git checkout main
VERSIONID=$(git rev-list HEAD --count main)
CHANGELOG_FILE="$CHANGELOG_DIR/$VERSIONID.txt"
git diff --exit-code || echo -e "\nWARNING WARNING WARNING WARNING WARNING: this branch contains uncommit changes."
#TODO Execute all tests: we are destroying data, running on a real device.
# ./gradlew connectedCheck
# Changelog
echo -e "\nPrevious changelog\n------------"
PREVIOUS_CHANGELOG=$(find "$CHANGELOG_DIR" -iname "[[:digit:]]**.txt" | sort -n | tail -1)
sed 's/^/>/' "$PREVIOUS_CHANGELOG"
#For F-Droid, a version starts with a 'v'
read -p $'\n\nPlease enter the versionname (e.g., v3.9.5):\n> v' VERSIONNAME
VERSIONNAME="v$VERSIONNAME"
cp "$CHANGELOG_DIR/next_release.txt" "$CHANGELOG_FILE"
sed -i "s/(versionName)/$VERSIONNAME/" "$CHANGELOG_FILE"
echo -e "\nGit commit since last release\n------------"
git log "$(git describe --tags --abbrev=0)..HEAD" --no-merges --oneline
echo -e "\nPlease edit the changelog for the new release"
gedit -w "$CHANGELOG_FILE"
git add "$CHANGELOG_FILE"
echo "Updating build.gradle"
sed -i s/versionCode\ .*/versionCode\ "$VERSIONID"/g build.gradle
sed -i s/versionName\ .*/versionName\ "\"$VERSIONNAME\""/g build.gradle
git diff
COMMIT_MESSAGE="Release: $VERSIONNAME"
echo -e "\nCommit message will be: '$COMMIT_MESSAGE'"
echo -e "\nPress ENTER, if everything looks good."
read
git add -u
git commit -m "$COMMIT_MESSAGE"
echo -e "Next steps:\n1. Push to Github\n2. Create release on Github: https://github.com/OpenTracksApp/OpenTracks/releases"
+7 -12
View File
@@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.2'
classpath 'com.android.tools.build:gradle:4.0.1'
}
}
@@ -45,14 +45,10 @@ android {
compileSdkVersion 30
buildFeatures {
viewBinding = true
}
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 3821
versionName "v3.14.2"
versionCode 3544
versionName "v3.9.3"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -86,18 +82,17 @@ android {
}
dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha03'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'com.google.android.material:material:1.2.1'
implementation 'com.android.support.constraint:constraint-layout:2.0.4'
implementation 'com.google.android.material:material:1.2.0'
implementation 'com.android.support.constraint:constraint-layout:2.0.1'
androidTestImplementation 'androidx.test:core:1.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'org.mockito:mockito-android:3.7.7'
androidTestImplementation 'org.mockito:mockito-android:3.4.6'
}
+9 -9
View File
@@ -10,11 +10,15 @@ endlegend
[*] --> TrackListActivity
TrackListActivity --> TrackRecordingActivity : Start recording
TrackListActivity --> TrackRecordedActivity : Show track
TrackListActivity --> SearchListActivity
TrackListActivity --> MarkerListActivity : Show all markers
TrackListActivity --> AggregatedStatisticsActivity : Aggregated stats
TrackListActivity --> TrackRecordedActivity : Show track
TrackListActivity --> TrackDeleteActivity
SearchListActivity --> MarkerDetailActivity : Search result
SearchListActivity --> TrackRecordedActivity : Search result
TrackRecordedActivity --> TrackDeleteActivity
TrackRecordedActivity --> TrackEditActivity
TrackRecordedActivity --> MarkerListActivity
@@ -42,14 +46,10 @@ MarkerDetailActivity --> CoordinateOnMapIntent : URI
SettingsActivity --> AboutActivity
SettingsActivity --> HelpActivity
SettingsActivity --> ImportActivity
SettingsActivity --> ExportActivity
SettingsActivity --> DirectoryChooserActivity.ImportDirectoryChooserActivity
SettingsActivity --> DirectoryChooserActivity.ExportDirectoryChooserActivity
SettingsActivity --> DirectoryChooserActivity.DefaultTrackExportDirectoryChooserActivity
DirectoryChooserActivity.ImportDirectoryChooserActivity --> ImportActivity
DirectoryChooserActivity.ExportDirectoryChooserActivity --> ExportActivity
FileImportIntent --> ImportActivity
SettingsActivity --> DirectoryChooserActivity
SettingsActivity --> DirectoryChooserActivity
DirectoryChooserActivity$ImportDirectoryChooserActivity --> ImportActivity
DirectoryChooserActivity$ExportDirectoryChooserActivity --> ExportActivity
@enduml
+1 -2
View File
@@ -4,8 +4,7 @@ LocationManager --> TrackRecordingService
ElevationSumManager --> TrackRecordingService
TrackRecordingService --> TrackRecordingServiceConnection
TrackRecordingService --> CustomContentProvider
CustomContentProvider --> Database
TrackRecordingService --> Database
Database --> CustomContentProvider
CustomContentProvider --> TrackDataHub
+3 -33
View File
@@ -8,7 +8,7 @@ class Track {
-icon
}
Track o-- TrackPoint
Track o-- Marker
Track o-- Waypoint
Track "1" - "1" TripStatistics
class TripStatistics {
@@ -21,12 +21,7 @@ class TripStatistics {
- totalElevationGain
}
note left of TripStatistics
The time is based upon device time.
end note
class TrackPoint {
class TrackPoint extends android.location.Location {
- id
- trackId
- longitude
@@ -41,32 +36,7 @@ class TrackPoint {
- sensor_power
}
note right of TrackPoint
TODO
TrackPoints are mixing two concepts:
1. a GPS measurement provided by the GPS hardware
2. and also segment markers.
These are marked by PAUSE / RESUME TrackPoints.
While exporting PAUSE/RESUME are not used to create these segments and these are partly recreated when imported.
However, the timestamp of PAUSE/RESUME is not exported and can therefore not be restored.
This prevents us from restoring the exact TripStatistics.
It is also noteworthy that neither PAUSE/RESUME TrackPoints are used for computing the TrackStatistics.
Only the timestamp to _create_ those is used.
end note
note left of TrackPoint
TODO
The time stored in TrackPoints is either:
1. created via GPS: provided by the GPS hardware
2. created by the user (pause/resume): device time.
We know that the GPS time and device time are often not in sync.
end note
TrackPoint "1" - "1" android.location.Location
class Marker {
class Waypoint {
- id
- trackId
- name
@@ -5,6 +5,6 @@ Funktionen & Features:
* zeichnet Sport und andere Outdooraktivitäten auf
* unterstützt das Markieren interessanter Orte, inkl. Foto-Funktion
* zeichnet deine Herzfrequenz auf (Bluetooth LE)
* nutze beim Radfahren deine Trittfrequenz- und Geschwindigkeitssensoren sowie Leistungsmesser (Bluetooth LE)
* nutze beim Radfahren deine Trittfrequenz- und Geschwindigkeitssensoren (Bluetooth LE)
* exportiert deine Daten als KML oder GPX
* funktioniert ganz ohne Internet
@@ -1,10 +0,0 @@
v3.9.4: OpenTracks
Changes:
- Added interval computation
- Voice announcements present last interval statistics
- Settings: Bluetooth icon is animated
Bugfix:
- GPS was not turned off when stopping a recording
- Show on map via KML/KMZ/GPX was broken due to refactoring
@@ -1,8 +0,0 @@
v3.9.5: OpenTracks
Changes:
- UI shows speed only with one fractional digit
Bugfix:
- App crash in search (due to Track.Id refactoring)
- Track list did now show if a recording was ongoing (due to Track.Id refactoring)
@@ -1,10 +0,0 @@
v3.10.0: OpenTracks
Changes:
- Elevation gain via barometer
- Add splash screen
- Android 11/F-Droid: downgrade to targetSDK 29 (v1 signature)
see https://gitlab.com/fdroid/fdroidserver/-/issues/827
Bugfix:
- Several fixes due to trackId refactoring
@@ -1,10 +0,0 @@
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
@@ -1,4 +0,0 @@
v3.11.1: OpenTracks
Bugfix:
- Voice announcements reported wrong statistics
@@ -1,22 +0,0 @@
v3.12.0: OpenTracks
Changes:
- Bluetooth sensor addresses
- Intervals: show elevation gain
- Export: default export directory
- Export: automatic export after finishing a track
- Manual import/export: new user interface
- Manual export: option to overwrite on manual export
Faster:
- Track lists loading
- Deleting multiple tracks
Bugfix:
- Fixed intent filters for import
- KMZ could not be parsed
- Export: fix crash on Android 11
Developer:
- Use ViewBindings
- Database enforces foreign key and uses cascading deletes
@@ -1,10 +0,0 @@
v3.13.0: OpenTracks
Changes:
- Sensor: support for powermeter (BLE)
- Search by track and marker (in lists)
- Combined search removed
Bugfix:
- Import crashed in UI
- Images of markers were not imported
@@ -1,5 +0,0 @@
v3.13.1: OpenTracks
Bugfix:
- Crash on record start: minify/shrink removed a drawable
- Automatic export failed silently (mainly remote destinations)
@@ -1,5 +0,0 @@
v3.13.2: OpenTracks
Bugfix:
- Crash in landscape mode
@@ -1,10 +0,0 @@
v3.14.0: OpenTracks
Changes:
- Properly support for right-to-left
- Import: faster cleanup after import failure
- Text in charts scales according to Android settings
- Heart rate: support for Huami devices (requires Gadgetbridge)
Developer:
- Import is now handled in-memory
@@ -1,14 +0,0 @@
v3.14.1: OpenTracks
Changes:
- Added elevation loss (via barometer)
- Updated translations (cz, gl, it, nb, pl, pt, sp)
- Markers can be shared from list
- Actionbar icon (top left) to go back
-
Bugfix:
- Potential crash on import
Developer:
- Implemented basic EspressoUI test
- Reference resourceIds via if rather than switch/case
@@ -1,10 +0,0 @@
v3.14.2: OpenTracks
Changes:
- Downgrade database from upcoming release v3.15.0
- Import/Export UI improved
- Import offers to show imported track
- Marker list performance improved
Developer:
- Track.Id/Marker.Id: use null
@@ -1,4 +1,4 @@
(versionName): OpenTracks
(tbd): OpenTracks
Changes:
-
@@ -6,6 +6,5 @@ Features:
* Supports Bluetooth LE sensors:
* heart rate
* cycling: speed and cadence
* cycling: power meter
* Export data: export tracks either as KML or GPX
* No Internet access: Internet is not used
Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+2
View File
@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
Vendored
+104 -103
View File
@@ -1,103 +1,104 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

@@ -1,168 +0,0 @@
package de.dennisguse.opentracks;
import android.view.MotionEvent;
import android.view.View;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.action.MotionEvents;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import com.google.android.material.tabs.TabLayout;
import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoUITest {
@Rule
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant("android.permission.ACCESS_FINE_LOCATION");
@LargeTest
@Test
public void record_pause_resume_stop() {
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
trackControllerRecordButton.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_controller_stop));
// wait; stay recording
trackControllerRecordButton.perform(waitFor(5000));
//pause
trackControllerRecordButton.perform(veryLongTouch(1600));
// wait; stay paused
trackControllerRecordButton.perform(waitFor(1000));
// resume
trackControllerRecordButton.perform(click());
// wait; stay recording
trackControllerRecordButton.perform(waitFor(1000));
// stop;
trackControllerStopButton.perform(veryLongTouch(1600));
}
}
@LargeTest
@Test
public void record_move_through_tabs() {
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
trackControllerRecordButton.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_controller_stop));
tabLayout.perform(selectTabAtIndex(1));
tabLayout.perform(waitFor(1000));
tabLayout.perform(selectTabAtIndex(2));
tabLayout.perform(waitFor(1000));
tabLayout.perform(selectTabAtIndex(3));
tabLayout.perform(waitFor(1000));
tabLayout.perform(selectTabAtIndex(0));
tabLayout.perform(waitFor(1000));
// stop
trackControllerStopButton.perform(veryLongTouch(1600));
}
}
private static ViewAction veryLongTouch(final int duration_ms) {
return new ViewAction() {
@Override
public String getDescription() {
return "Perform long touch.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
// Get view absolute position
int[] location = new int[2];
view.getLocationOnScreen(location);
// Offset coordinates by view position
float[] coordinates = new float[]{location[0] + 1, location[1] + 1};
// Send down event, pause, and send up
MotionEvent down = MotionEvents.sendDown(uiController, coordinates, new float[]{1f, 1f}).down;
uiController.loopMainThreadForAtLeast(duration_ms);
MotionEvents.sendUp(uiController, down, coordinates);
}
};
}
private static ViewAction waitFor(final long duration_ms) {
return new ViewAction() {
@Override
public String getDescription() {
return "Wait for milliseconds.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
uiController.loopMainThreadForAtLeast(duration_ms);
}
};
}
private static ViewAction selectTabAtIndex(final int index) {
return new ViewAction() {
@Override
public String getDescription() {
return "Selecting tab.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, View view) {
TabLayout tabLayout = (TabLayout) view;
tabLayout.getTabAt(index).select();
}
};
}
}
@@ -14,7 +14,7 @@
* the License.
*/
package de.dennisguse.opentracks.fragments;
package de.dennisguse.opentracks;
import android.location.Location;
@@ -19,16 +19,13 @@ import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Tests {@link ChartValueSeries}.
*
@@ -48,9 +45,7 @@ public class ChartValueSeriesTest {
R.string.description_elevation_metric,
R.string.description_elevation_imperial,
R.color.chart_elevation_fill,
R.color.chart_elevation_border,
15,
18) {
R.color.chart_elevation_border) {
@Override
double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getElevation();
@@ -65,16 +60,16 @@ public class ChartValueSeriesTest {
@Test
public void testInitialConditions() {
assertEquals(1, series.getInterval());
assertEquals(0, series.getMinMarkerValue());
assertEquals(5, series.getMaxMarkerValue());
assertTrue(series.isEnabled());
Assert.assertEquals(1, series.getInterval());
Assert.assertEquals(0, series.getMinMarkerValue());
Assert.assertEquals(5, series.getMaxMarkerValue());
Assert.assertTrue(series.isEnabled());
}
@Test
public void testEnabled() {
series.setEnabled(false);
assertFalse(series.isEnabled());
Assert.assertFalse(series.isEnabled());
}
@Test
@@ -82,9 +77,9 @@ public class ChartValueSeriesTest {
series.update(new ChartPoint(0));
series.update(new ChartPoint(10));
series.updateDimension();
assertEquals(100, series.getInterval());
assertEquals(0, series.getMinMarkerValue());
assertEquals(500, series.getMaxMarkerValue());
Assert.assertEquals(100, series.getInterval());
Assert.assertEquals(0, series.getMinMarkerValue());
Assert.assertEquals(500, series.getMaxMarkerValue());
}
@Test
@@ -92,9 +87,9 @@ public class ChartValueSeriesTest {
series.update(new ChartPoint(0));
series.update(new ChartPoint(901));
series.updateDimension();
assertEquals(1000, series.getInterval());
assertEquals(0, series.getMinMarkerValue());
assertEquals(5000, series.getMaxMarkerValue());
Assert.assertEquals(1000, series.getInterval());
Assert.assertEquals(0, series.getMinMarkerValue());
Assert.assertEquals(5000, series.getMaxMarkerValue());
}
@Test
@@ -102,8 +97,8 @@ public class ChartValueSeriesTest {
series.update(new ChartPoint(220));
series.update(new ChartPoint(250));
series.updateDimension();
assertEquals(100, series.getInterval());
assertEquals(200, series.getMinMarkerValue());
assertEquals(700, series.getMaxMarkerValue());
Assert.assertEquals(100, series.getInterval());
Assert.assertEquals(200, series.getMinMarkerValue());
Assert.assertEquals(700, series.getMaxMarkerValue());
}
}
@@ -21,6 +21,7 @@ import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -30,8 +31,6 @@ import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
import static org.junit.Assert.assertEquals;
/**
* Tests for {@link DescriptionGenerator}.
*
@@ -64,7 +63,6 @@ public class DescriptionGeneratorTest {
stats.setMaxElevation(550);
stats.setMinElevation(-500);
stats.setTotalElevationGain(6000);
stats.setTotalElevationLoss(6000);
stats.setStartTime_ms(START_TIME);
track.setTrackStatistics(stats);
track.setCategory("hiking");
@@ -85,10 +83,9 @@ public class DescriptionGeneratorTest {
+ "Max elevation: 550 m (1804 ft)<br>"
+ "Min elevation: -500 m (-1640 ft)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Elevation loss: 6000 m (19685 ft)<br>"
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "<br>";
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
@Test
@@ -102,7 +99,6 @@ public class DescriptionGeneratorTest {
stats.setMaxElevation(Double.POSITIVE_INFINITY);
stats.setMinElevation(Double.NEGATIVE_INFINITY);
stats.setTotalElevationGain(6000);
stats.setTotalElevationLoss(6000);
stats.setStartTime_ms(START_TIME);
track.setTrackStatistics(stats);
track.setCategory("hiking");
@@ -121,10 +117,9 @@ public class DescriptionGeneratorTest {
+ "Average moving pace: 0:15 min/km (0:24 min/mi)<br>"
+ "Fastest pace: 0:10 min/km (0:16 min/mi)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Elevation loss: 6000 m (19685 ft)<br>"
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "<br>";
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
@@ -135,7 +130,7 @@ public class DescriptionGeneratorTest {
public void testWriteDistance() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeDistance(1100, builder, R.string.description_total_distance, "<br>");
assertEquals("Total distance: 1.10 km (0.7 mi)<br>", builder.toString());
Assert.assertEquals("Total distance: 1.10 km (0.7 mi)<br>", builder.toString());
}
/**
@@ -145,7 +140,7 @@ public class DescriptionGeneratorTest {
public void testWriteTime() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeTime(1000, builder, R.string.description_total_time, "<br>");
assertEquals("Total time: 00:01<br>", builder.toString());
Assert.assertEquals("Total time: 00:01<br>", builder.toString());
}
/**
@@ -155,7 +150,7 @@ public class DescriptionGeneratorTest {
public void testWriteSpeed() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeSpeed(1.1, builder, R.string.description_average_speed, "\n");
assertEquals("Average speed: 3.96 km/h (2.5 mi/h)\n", builder.toString());
Assert.assertEquals("Average speed: 3.96 km/h (2.5 mi/h)\n", builder.toString());
}
/**
@@ -165,7 +160,7 @@ public class DescriptionGeneratorTest {
public void testWriteElevation() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writeElevation(4.2, builder, R.string.description_min_elevation, "<br>");
assertEquals("Min elevation: 4 m (14 ft)<br>", builder.toString());
Assert.assertEquals("Min elevation: 4 m (14 ft)<br>", builder.toString());
}
/**
@@ -175,6 +170,6 @@ public class DescriptionGeneratorTest {
public void testWritePace() {
StringBuilder builder = new StringBuilder();
descriptionGenerator.writePace(1.1, builder, R.string.description_average_pace_in_minute, "\n");
assertEquals("Average pace: 15:09 min/km (24:23 min/mi)\n", builder.toString());
Assert.assertEquals("Average pace: 15:09 min/km (24:23 min/mi)\n", builder.toString());
}
}
@@ -0,0 +1,319 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.content;
import android.content.ContentUris;
import android.location.Location;
import android.net.Uri;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.provider.ProviderTestRule;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.content.provider.CustomSQLiteOpenHelper;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* Tests for {@link SearchEngine}.
* These are not meant to be quality tests, but instead feature-by-feature tests.
* In other words, they don't test the mixing of different score boostings, just each boosting separately.
*
* @author Rodrigo Damazio
*/
public class SearchEngineTest {
private static final String TAG = SearchEngineTest.class.getSimpleName();
private static final Location HERE = new Location("gps");
private static final long NOW = SearchEngine.OLDEST_ALLOWED_TIMESTAMP + 1000;
@Rule
public ProviderTestRule sqliteContentProviderRule = new ProviderTestRule.Builder(CustomContentProvider.class, ContentProviderUtils.AUTHORITY_PACKAGE).setPrefix(TAG).build();
private ContentProviderUtils providerUtils;
private SearchEngine engine;
@Before
public void setUp() {
providerUtils = new ContentProviderUtils(sqliteContentProviderRule.getResolver());
engine = new SearchEngine(providerUtils);
}
@After
public void tearDown() {
InstrumentationRegistry.getInstrumentation().getTargetContext().deleteDatabase(TAG + CustomSQLiteOpenHelper.DATABASE_NAME);
}
private Track.Id insertTrack(String title, String description, String category, long hoursAgo) {
Track track = new Track();
track.setName(title);
track.setDescription(description);
track.setCategory(category);
TrackStatistics stats = track.getTrackStatistics();
if (hoursAgo > 0) {
// Started twice hoursAgo, so the average time is hoursAgo.
stats.setStartTime_ms(NOW - hoursAgo * 1000L * 60L * 60L * 2);
stats.setStopTime_ms(NOW);
}
Uri uri = providerUtils.insertTrack(track);
return new Track.Id(ContentUris.parseId(uri));
}
private Track.Id insertTrack(String title, String description, String category) {
return insertTrack(title, description, category, -1);
}
private Track.Id insertTrack(String title, long hoursAgo) {
return insertTrack(title, "", "", hoursAgo);
}
private Waypoint.Id insertWaypoint(String title, String description, String category, double distance, long hoursAgo, Track.Id trackId) {
Location location = new Location(HERE);
location.setLatitude(location.getLatitude() + distance);
location.setLongitude(location.getLongitude() + distance);
if (hoursAgo >= 0) {
location.setTime(NOW - hoursAgo * 1000L * 60L * 60L);
}
Waypoint waypoint = new Waypoint(location);
waypoint.setName(title);
waypoint.setDescription(description);
waypoint.setCategory(category);
waypoint.setTrackId(trackId);
Uri uri = providerUtils.insertWaypoint(waypoint);
return new Waypoint.Id(ContentUris.parseId(uri));
}
private Waypoint.Id insertWaypoint(String title, String description, String category) {
return insertWaypoint(title, description, category, 0.0, -1, null);
}
private Waypoint.Id insertWaypoint(String title, double distance) {
return insertWaypoint(title, "", "", distance, -1, null);
}
private Waypoint.Id insertWaypoint(String title, long hoursAgo) {
return insertWaypoint(title, "", "", 0.0, hoursAgo, null);
}
private Waypoint.Id insertWaypoint(String title, long hoursAgo, Track.Id trackId) {
return insertWaypoint(title, "", "", 0.0, hoursAgo, trackId);
}
@Test
public void testSearchText() {
// Insert 7 tracks (purposefully out of result order):
// - one which won't match
// - one which will match the description
// - one which will match the category
// - one which will match the title
// - one which will match in title and category
// - one which will match in title and description
// - one which will match in all fields
insertTrack("bb", "cc", "dd");
Track.Id descriptionMatchId = insertTrack("bb", "aa", "cc");
Track.Id categoryMatchId = insertTrack("bb", "cc", "aa");
Track.Id titleMatchId = insertTrack("aa", "bb", "cc");
Track.Id titleCategoryMatchId = insertTrack("aa", "bb", "ca");
Track.Id titleDescriptionMatchId = insertTrack("aa", "ba", "cc");
Track.Id allMatchId = insertTrack("aa", "ba", "ca");
SearchQuery query = new SearchQuery("a", null, null, NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Title > Description > Category.
assertTrackResults(results, allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId, categoryMatchId);
}
@Test
public void testSearchWaypointText() {
// Insert 7 waypoints (purposefully out of result order):
// - one which won't match
// - one which will match the description
// - one which will match the category
// - one which will match the title
// - one which will match in title and category
// - one which will match in title and description
// - one which will match in all fields
insertWaypoint("bb", "cc", "dd");
Waypoint.Id descriptionMatchId = insertWaypoint("bb", "aa", "cc");
Waypoint.Id categoryMatchId = insertWaypoint("bb", "cc", "aa");
Waypoint.Id titleMatchId = insertWaypoint("aa", "bb", "cc");
Waypoint.Id titleCategoryMatchId = insertWaypoint("aa", "bb", "ca");
Waypoint.Id titleDescriptionMatchId = insertWaypoint("aa", "ba", "cc");
Waypoint.Id allMatchId = insertWaypoint("aa", "ba", "ca");
SearchQuery query = new SearchQuery("a", null, null, NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Title > Description > Category.
assertWaypointResults(results, allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId, categoryMatchId);
}
@Test
public void testSearchMixedText() {
// Insert 5 entries (purposefully out of result order):
// - one waypoint which will match by description
// - one waypoint which won't match
// - one waypoint which will match by title
// - one track which won't match
// - one track which will match by title
Waypoint.Id descriptionWaypointId = insertWaypoint("bb", "aa", "cc");
insertWaypoint("bb", "cc", "dd");
Waypoint.Id titleWaypointId = insertWaypoint("aa", "bb", "cc");
insertTrack("bb", "cc", "dd");
Track.Id trackId = insertTrack("aa", "bb", "cc");
SearchQuery query = new SearchQuery("a", null, null, NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Title > Description > Category.
Assert.assertEquals(results.toString(), 3, results.size());
assertTrackResult(trackId, results.get(0));
assertWaypointResult(titleWaypointId, results.get(1));
assertWaypointResult(descriptionWaypointId, results.get(2));
}
@Test
public void testSearchWaypointDistance() {
// All results match text, but they're at difference distances from the user.
Waypoint.Id farFarAwayId = insertWaypoint("aa", 0.3);
Waypoint.Id nearId = insertWaypoint("ab", 0.1);
Waypoint.Id farId = insertWaypoint("ac", 0.2);
SearchQuery query = new SearchQuery("a", HERE, null, NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Distance order.
assertWaypointResults(results, nearId, farId, farFarAwayId);
}
public void testSearchTrackRecent() {
// All results match text, but they're were recorded at different times.
Track.Id oldestId = insertTrack("aa", 3);
Track.Id recentId = insertTrack("ab", 1);
Track.Id oldId = insertTrack("ac", 2);
SearchQuery query = new SearchQuery("a", null, null, NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Reverse time order.
assertTrackResults(results, recentId, oldId, oldestId);
}
@Test
public void testSearchWaypointRecent() {
// All results match text, but they're were recorded at different times.
Waypoint.Id oldestId = insertWaypoint("aa", 2);
Waypoint.Id recentId = insertWaypoint("ab", 0);
Waypoint.Id oldId = insertWaypoint("ac", 1);
SearchQuery query = new SearchQuery("a", null, null, NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Reverse time order.
assertWaypointResults(results, recentId, oldId, oldestId);
}
@Test
public void testSearchCurrentTrack() {
// All results match text, but one of them is the current track.
Track.Id currentId = insertTrack("ab", 1);
Track.Id otherId = insertTrack("aa", 1);
SearchQuery query = new SearchQuery("a", null, currentId, NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Current track should be demoted.
assertTrackResults(results, otherId, currentId);
}
@Test
public void testSearchCurrentTrackWaypoint() {
// All results match text, but one of them is in the current track.
Waypoint.Id otherId = insertWaypoint("aa", 1, new Track.Id(456));
Waypoint.Id currentId = insertWaypoint("ab", 1, new Track.Id(123));
SearchQuery query = new SearchQuery("a", null, new Track.Id(123), NOW);
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
// Waypoint in current track should be promoted.
assertWaypointResults(results, currentId, otherId);
}
private void assertTrackResult(Track.Id trackId, ScoredResult result) {
Assert.assertNotNull("Not a track", result.track);
Assert.assertNull("Ambiguous result", result.waypoint);
Assert.assertEquals(trackId, result.track.getId());
}
private void assertTrackResults(List<ScoredResult> results, Track.Id... trackIds) {
long[] longTrackIds = new long[trackIds.length];
for (int i = 0; i < trackIds.length; i++) {
longTrackIds[i] = trackIds[i].getId();
}
String errMsg = "Expected IDs=" + Arrays.toString(longTrackIds) + "; results=" + results;
Assert.assertEquals(results.size(), trackIds.length);
for (int i = 0; i < results.size(); i++) {
ScoredResult result = results.get(i);
Assert.assertNotNull(errMsg, result.track);
Assert.assertNull(errMsg, result.waypoint);
Assert.assertEquals(errMsg, trackIds[i], result.track.getId());
}
}
private void assertWaypointResult(Waypoint.Id waypointId, ScoredResult result) {
Assert.assertNotNull("Not a waypoint", result.waypoint);
Assert.assertNull("Ambiguous result", result.track);
Assert.assertEquals(waypointId, result.waypoint.getId());
}
private void assertWaypointResults(List<ScoredResult> results, Waypoint.Id... waypointIds) {
long[] longWaypointIds = new long[waypointIds.length];
for (int i = 0; i < waypointIds.length; i++) {
longWaypointIds[i] = waypointIds[i].getId();
}
String errMsg = "Expected IDs=" + Arrays.toString(longWaypointIds) + "; results=" + results;
Assert.assertEquals(results.size(), waypointIds.length);
for (int i = 0; i < results.size(); i++) {
ScoredResult result = results.get(i);
Assert.assertNotNull(errMsg, result.waypoint);
Assert.assertNull(errMsg, result.track);
Assert.assertEquals(errMsg, waypointIds[i], result.waypoint.getId());
}
}
}
@@ -7,8 +7,6 @@ import android.util.Pair;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.util.FileUtils;
@@ -18,8 +16,6 @@ public class TestDataUtil {
public static final double INITIAL_LATITUDE = 37.0;
public static final double INITIAL_LONGITUDE = -57.0;
public static final double ALTITUDE_INTERVAL = 2.5;
public static final float ELEVATION_GAIN = 3;
public static final float ELEVATION_LOSS = 3;
/**
* Create a track without any trackPoints.
@@ -38,19 +34,19 @@ public class TestDataUtil {
* @param trackId the trackId of the track
* @param numPoints the trackPoints number in the track
*/
public static Pair<Track, List<TrackPoint>> createTrack(Track.Id trackId, int numPoints) {
public static Pair<Track, TrackPoint[]> createTrack(Track.Id trackId, int numPoints) {
Track track = createTrack(trackId);
List<TrackPoint> trackPoints = new ArrayList<>(numPoints);
TrackPoint[] trackPoints = new TrackPoint[numPoints];
for (int i = 0; i < numPoints; i++) {
trackPoints.add(createTrackPoint(i));
trackPoints[i] = createTrackPoint(i);
}
return new Pair<>(track, trackPoints);
}
public static Track createTrackAndInsert(ContentProviderUtils contentProviderUtils, Track.Id trackId, int numPoints) {
Pair<Track, List<TrackPoint>> pair = createTrack(trackId, numPoints);
Pair<Track, TrackPoint[]> pair = createTrack(trackId, numPoints);
insertTrackWithLocations(contentProviderUtils, pair.first, pair.second);
@@ -69,13 +65,12 @@ public class TestDataUtil {
trackPoint.setAccuracy((float) i / 100.0f);
trackPoint.setAltitude(i * ALTITUDE_INTERVAL);
trackPoint.setTime(i + 1);
trackPoint.setSpeed(5f + (i / 10f));
trackPoint.setSpeed(5f + (i / 10));
trackPoint.setHeartRate_bpm(100f + i);
trackPoint.setCyclingCadence_rpm(200f + i);
trackPoint.setCyclingCadence_rpm(300f + i);
trackPoint.setPower(400f + i);
trackPoint.setElevationGain(ELEVATION_GAIN);
trackPoint.setElevationLoss(ELEVATION_LOSS);
return trackPoint;
}
@@ -85,25 +80,25 @@ public class TestDataUtil {
* @param track track to be inserted
* @param trackPoints trackPoints to be inserted
*/
public static void insertTrackWithLocations(ContentProviderUtils contentProviderUtils, Track track, List<TrackPoint> trackPoints) {
public static void insertTrackWithLocations(ContentProviderUtils contentProviderUtils, Track track, TrackPoint[] trackPoints) {
contentProviderUtils.insertTrack(track);
contentProviderUtils.bulkInsertTrackPoint(trackPoints, track.getId());
}
/**
* Creates a Marker with a photo.
* Creates a Waypoint with a photo.
*
* @param context The context.
* @param trackId The track id.
* @param location The location.
* @return the Marker created.
* @return the Waypoint created.
*/
public static Marker createMarkerWithPhoto(Context context, Track.Id trackId, Location location) throws IOException {
public static Waypoint createWaypointWithPhoto(Context context, Track.Id trackId, Location location) throws IOException {
File dstFile = new File(FileUtils.getImageUrl(context, trackId));
dstFile.createNewFile();
Uri photoUri = FileUtils.getUriForFile(context, dstFile);
String photoUrl = photoUri.toString();
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, 0.0, 0, location, photoUrl);
return new Waypoint("Waypoint name", "Waypoint description", "Waypoint category", "", trackId, 0.0, 0, location, photoUrl);
}
}
@@ -25,9 +25,9 @@ import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
import org.junit.Test;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -67,7 +67,7 @@ public class CustomContentProviderTest {
assertEquals(TrackPointsColumns.CONTENT_TYPE, customContentProvider.getType(TrackPointsColumns.CONTENT_URI_BY_ID));
assertEquals(TrackPointsColumns.CONTENT_ITEMTYPE, customContentProvider.getType(ContentUris.appendId(TrackPointsColumns.CONTENT_URI_BY_TRACKID.buildUpon(), 1).build()));
assertEquals(MarkerColumns.CONTENT_TYPE, customContentProvider.getType(MarkerColumns.CONTENT_URI));
assertEquals(MarkerColumns.CONTENT_ITEMTYPE, customContentProvider.getType(ContentUris.appendId(MarkerColumns.CONTENT_URI.buildUpon(), 1).build()));
assertEquals(WaypointsColumns.CONTENT_TYPE, customContentProvider.getType(WaypointsColumns.CONTENT_URI));
assertEquals(WaypointsColumns.CONTENT_ITEMTYPE, customContentProvider.getType(ContentUris.appendId(WaypointsColumns.CONTENT_URI.buildUpon(), 1).build()));
}
}
@@ -5,8 +5,8 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import androidx.annotation.VisibleForTesting;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -15,14 +15,12 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -40,21 +38,39 @@ public class CustomSQLiteOpenHelperTest {
private final Context context = ApplicationProvider.getApplicationContext();
/**
* Get the SQL create statements for all SQLite elements of type (ordered by name).
* Get the SQL create statements for all tables (ordered by name).
*
* @param type index, table
* @return Map(name, SQL)
* @return Map(TableName, SQL)
*/
public static Map<String, String> getSQL(SQLiteDatabase db, String type) {
HashMap<String, String> sqlMap = new HashMap<>();
try (Cursor cursor = db.query("sqlite_master", new String[]{"name", "SQL"}, "type=?", new String[]{type}, null, null, "name")) {
@VisibleForTesting
public static Map<String, String> getTableSQL(SQLiteDatabase db) {
HashMap<String, String> tableSQL = new HashMap<>();
try (Cursor cursor = db.query("sqlite_master", new String[]{"name", "SQL"}, "name IN ('" + TracksColumns.TABLE_NAME + "', '" + TrackPointsColumns.TABLE_NAME + "', '" + WaypointsColumns.TABLE_NAME + "')", null, null, null, "name")) {
if (cursor != null) {
while (cursor.moveToNext()) {
sqlMap.put(cursor.getString(0), cursor.getString(1));
tableSQL.put(cursor.getString(0), cursor.getString(1));
}
}
}
return sqlMap;
return tableSQL;
}
/**
* Get the SQL create statements for all indices (ordered by name).
*
* @return Map(tableName, SQL)
*/
@VisibleForTesting
public static Map<String, String> getIndexSQL(SQLiteDatabase db) {
HashMap<String, String> indexSQL = new HashMap<>();
try (Cursor cursor = db.rawQuery("SELECT tbl_name, SQL FROM sqlite_master WHERE type = 'index'", null)) {
if (cursor != null) {
while (cursor.moveToNext()) {
indexSQL.put(cursor.getString(0), cursor.getString(1));
}
}
}
return indexSQL;
}
/**
@@ -90,10 +106,10 @@ public class CustomSQLiteOpenHelperTest {
assertTrue(hasSqlCreate(db, TrackPointsColumns.CREATE_TABLE));
assertTrue(hasSqlCreate(db, TrackPointsColumns.CREATE_TABLE_INDEX));
assertTrue(hasSqlCreate(db, MarkerColumns.CREATE_TABLE));
assertTrue(hasSqlCreate(db, MarkerColumns.CREATE_TABLE_INDEX));
assertTrue(hasSqlCreate(db, WaypointsColumns.CREATE_TABLE));
assertTrue(hasSqlCreate(db, WaypointsColumns.CREATE_TABLE_INDEX));
} catch (Exception e) {
fail("Database could not be created: " + e);
fail();
}
}
@@ -105,8 +121,8 @@ public class CustomSQLiteOpenHelperTest {
Map<String, String> tableByUpgrade;
Map<String, String> indicesByUpgrade;
try (SQLiteDatabase dbUpgraded = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase()) {
tableByUpgrade = getSQL(dbUpgraded, "table");
indicesByUpgrade = getSQL(dbUpgraded, "index");
tableByUpgrade = getTableSQL(dbUpgraded);
indicesByUpgrade = getIndexSQL(dbUpgraded);
}
context.deleteDatabase(DATABASE_NAME);
@@ -114,25 +130,23 @@ public class CustomSQLiteOpenHelperTest {
Map<String, String> tablesByCreate;
Map<String, String> indicesByCreate;
try (SQLiteDatabase dbCreated = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase()) {
tablesByCreate = getSQL(dbCreated, "table");
indicesByCreate = getSQL(dbCreated, "index");
tablesByCreate = getTableSQL(dbCreated);
indicesByCreate = getIndexSQL(dbCreated);
}
// then - verify table structure
int tableCount = 3 + 2; //Three with data tables + two SQLite
assertEquals(tableCount, tableByUpgrade.size());
assertEquals(3, tableByUpgrade.size());
assertEquals(tableByUpgrade.size(), tablesByCreate.size());
assertEquals(tablesByCreate.get(TracksColumns.TABLE_NAME), tableByUpgrade.get(TracksColumns.TABLE_NAME));
assertEquals(tablesByCreate.get(TrackPointsColumns.TABLE_NAME), tableByUpgrade.get(TrackPointsColumns.TABLE_NAME));
assertEquals(tablesByCreate.get(MarkerColumns.TABLE_NAME), tableByUpgrade.get(MarkerColumns.TABLE_NAME));
assertEquals(tablesByCreate.get(WaypointsColumns.TABLE_NAME), tableByUpgrade.get(WaypointsColumns.TABLE_NAME));
// then - verify custom indices
assertEquals(3, indicesByCreate.size());
assertEquals(indicesByUpgrade.get(TracksColumns.TABLE_NAME), indicesByCreate.get(TracksColumns.TABLE_NAME));
assertEquals(indicesByUpgrade.get(TrackPointsColumns.TABLE_NAME), indicesByCreate.get(TrackPointsColumns.TABLE_NAME));
assertEquals(indicesByUpgrade.get(MarkerColumns.TABLE_NAME), indicesByCreate.get(MarkerColumns.TABLE_NAME));
assertEquals(indicesByUpgrade.get(WaypointsColumns.TABLE_NAME), indicesByCreate.get(WaypointsColumns.TABLE_NAME));
}
@Test
@@ -144,8 +158,8 @@ public class CustomSQLiteOpenHelperTest {
Map<String, String> tablesByDowngrade;
Map<String, String> indicesByDowngrade;
try (SQLiteDatabase db = new CustomSQLiteOpenHelper(context, DATABASE_NAME, 23).getReadableDatabase()) {
tablesByDowngrade = getSQL(db, "table");
indicesByDowngrade = getSQL(db, "index");
tablesByDowngrade = getTableSQL(db);
indicesByDowngrade = getIndexSQL(db);
}
// then - verify table structure
@@ -168,101 +182,6 @@ public class CustomSQLiteOpenHelperTest {
}
}
@Test
public void upgrade_data_to_30() {
// given: a track in version 29
createVersion23();
try (SQLiteDatabase db29 = new CustomSQLiteOpenHelper(context, DATABASE_NAME, 29).getWritableDatabase()) {
db29.beginTransaction();
db29.execSQL("INSERT INTO tracks (_id) VALUES (1)");
// Record -> stop
db29.execSQL("INSERT INTO tracks (_id) VALUES (2)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (2, 2.1, 2.1)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (2, 2.2, 2.2)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (2, 2.3, 2.3)");
// Record -> pause -> stop
db29.execSQL("INSERT INTO tracks (_id) VALUES (3)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (3, 3.1, 3.1)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (3, 3.2, 3.2)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (3, 0.0, 100)"); //manual PAUSE
// Record -> segment marker (distance to previous) -> pause -> resume -> stop
db29.execSQL("INSERT INTO tracks (_id) VALUES (4)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 4.1, 4.1)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 4.2, 4.2)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 0.0, 100 * 1E6)"); //SegmentEndMarker; will be deleted
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 4.3, 4.3)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 4.4, 4.4)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 0.0, 100 * 1E6)"); //manual PAUSE
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 0.0, 200 * 1E6)"); //manual RESUME
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 4.5, 4.5)");
db29.execSQL("INSERT INTO trackpoints (trackId, longitude, latitude) VALUES (4, 4.6, 4.6)");
db29.setTransactionSuccessful();
db29.endTransaction();
}
// when / then
try (SQLiteDatabase db30 = new CustomSQLiteOpenHelper(context, DATABASE_NAME, 30).getWritableDatabase()) {
{
// Track 1
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables("trackpoints");
queryBuilder.appendWhere("trackid = 1");
try (Cursor cursor = queryBuilder.query(db30, null, null, null, null, null, "_id")) {
assertEquals(0, cursor.getCount());
}
}
{
// Track 2
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables("trackpoints");
queryBuilder.appendWhere("trackid = 2");
try (Cursor cursor = queryBuilder.query(db30, null, null, null, null, null, "_id")) {
assertEquals(3, cursor.getCount());
}
}
{
// Track 3
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables("trackpoints");
queryBuilder.appendWhere("trackid = 3");
try (Cursor cursor = queryBuilder.query(db30, null, null, null, null, null, "_id")) {
assertEquals(3, cursor.getCount());
}
}
{
// Track 4
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables("trackpoints");
queryBuilder.appendWhere("trackid = 4");
try (Cursor cursor = queryBuilder.query(db30, null, null, null, null, null, "_id")) {
assertEquals(8, cursor.getCount());
List<Integer> types = new ArrayList<>();
List<Double> latitude = new ArrayList<>();
cursor.moveToFirst();
do {
types.add(cursor.getInt(cursor.getColumnIndexOrThrow("type")));
if (!cursor.isNull(cursor.getColumnIndexOrThrow("latitude"))) {
latitude.add(cursor.getDouble(cursor.getColumnIndexOrThrow("latitude")));
} else {
latitude.add(-1.0);
}
} while (cursor.moveToNext());
assertEquals(List.of(0, 0, -1, 0, 1, -2, 0, 0), types);
assertEquals(List.of(4.1, 4.2, 4.3, 4.4, -1.0, -1.0, 4.5, 4.6), latitude);
}
}
}
}
private void createVersion23() {
// Manually create database schema with version 23 (base version)
SQLiteDatabase dbBase = new SQLiteOpenHelper(context, DATABASE_NAME, null, 23) {
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.content.provider;
import android.net.Uri;
import android.util.Pair;
import org.junit.Assert;
import org.junit.Test;
import java.util.HashSet;
@@ -11,19 +12,17 @@ import java.util.Set;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import static org.junit.Assert.assertEquals;
public class ShareContentProviderTest {
@Test
public void testCreateAndParseURI_invalid() {
public void testCreateandParseURI_invalid() {
Uri uri = Uri.parse("content://de.dennisguse.opentracks.debug.content/tracks/1");
assertEquals(0, ShareContentProvider.parseURI(uri).size());
Assert.assertEquals(0, ShareContentProvider.parseURI(uri).size());
}
@Test
public void testCreateAndParseURI_valid() {
public void testCreateandParseURI_valid() {
Set<Track.Id> trackIds = new HashSet<>();
trackIds.add(new Track.Id(1));
trackIds.add(new Track.Id(3));
@@ -31,7 +30,7 @@ public class ShareContentProviderTest {
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "TrackName", TrackFileFormat.KML_ONLY_TRACK);
assertEquals(trackIds, ShareContentProvider.parseURI(shareURIandMIME.first));
Assert.assertEquals(trackIds, ShareContentProvider.parseURI(shareURIandMIME.first));
}
@Test
@@ -40,6 +39,6 @@ public class ShareContentProviderTest {
trackIds.add(new Track.Id(1));
Pair<Uri, String> shareURIandMIME = ShareContentProvider.createURI(trackIds, "../../&1=1", TrackFileFormat.KML_ONLY_TRACK);
assertEquals(Uri.parse("content://de.dennisguse.opentracks.debug.content/tracks/kml_only_track/1/..%2F..%2F%261%3D1.kml"), shareURIandMIME.first);
Assert.assertEquals(Uri.parse("content://de.dennisguse.opentracks.debug.content/tracks/kml_only_track/1/..%2F..%2F%261%3D1.kml"), shareURIandMIME.first);
}
}
@@ -2,15 +2,12 @@ package de.dennisguse.opentracks.content.sensor;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.util.UintUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
@RunWith(AndroidJUnit4.class)
public class SensorDataCyclingTest {
@@ -24,7 +21,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertEquals(60, current.getCadence_rpm(), 0.01);
Assert.assertEquals(60, current.getCadence_rpm(), 0.01);
}
@Test
@@ -37,7 +34,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertEquals(33.53, current.getCadence_rpm(), 0.01);
Assert.assertEquals(33.53, current.getCadence_rpm(), 0.01);
}
@Test
@@ -50,7 +47,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertEquals(0, current.getCadence_rpm(), 0.01);
Assert.assertEquals(0, current.getCadence_rpm(), 0.01);
}
@@ -64,7 +61,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertFalse(current.hasCadence_rpm());
Assert.assertFalse(current.hasCadence_rpm());
}
@Test
@@ -77,7 +74,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertEquals(60, current.getCadence_rpm(), 0.01);
Assert.assertEquals(60, current.getCadence_rpm(), 0.01);
}
@Test
@@ -90,7 +87,7 @@ public class SensorDataCyclingTest {
current.compute(previous);
// then
assertEquals(60, current.getCadence_rpm(), 0.01);
Assert.assertEquals(60, current.getCadence_rpm(), 0.01);
}
@Test
@@ -103,7 +100,7 @@ public class SensorDataCyclingTest {
current.compute(previous, 2150);
// then
assertEquals(1.20, current.getSpeed_mps(), 0.01);
Assert.assertEquals(1.20, current.getSpeed_mps(), 0.01);
}
@Test
@@ -116,34 +113,6 @@ public class SensorDataCyclingTest {
current.compute(previous, 2000);
// then
assertEquals(2, current.getSpeed_mps(), 0.01);
}
@Test
public void equals_speed_with_no_data() {
// given
SensorDataCycling.Speed previous = new SensorDataCycling.Speed("sensorAddress");
SensorDataCycling.Speed current = new SensorDataCycling.Speed("sensorAddress", "sensorName", 0, 2048);
// when
previous.toString();
// then
assertNotEquals(previous, current);
assertNotEquals(previous, previous);
}
@Test
public void equals_cadence_with_no_data() {
// given
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress");
SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 0, 2048);
// when
previous.toString();
// then
assertNotEquals(previous, current);
assertNotEquals(previous, previous);
Assert.assertEquals(2, current.getSpeed_mps(), 0.01);
}
}
@@ -23,18 +23,18 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.TrackStubUtils;
import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.UnitConversions;
import static org.junit.Assert.assertEquals;
/**
* Tests {@link ChartFragment}.
*
@@ -78,9 +78,9 @@ public class ChartFragmentTest {
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
// then
assertEquals(Float.NaN, point.getHeartRate(), 0.01);
assertEquals(Float.NaN, point.getCadence(), 0.01);
assertEquals(Float.NaN, point.getPower(), 0.01);
Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01);
Assert.assertEquals(Float.NaN, point.getCadence(), 0.01);
Assert.assertEquals(Float.NaN, point.getPower(), 0.01);
}
/**
@@ -98,9 +98,9 @@ public class ChartFragmentTest {
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
// then
assertEquals(100.0, point.getHeartRate(), 0.01);
assertEquals(101.0, point.getCadence(), 0.01);
assertEquals(102.0, point.getPower(), 0.01);
Assert.assertEquals(100.0, point.getHeartRate(), 0.01);
Assert.assertEquals(101.0, point.getCadence(), 0.01);
Assert.assertEquals(102.0, point.getPower(), 0.01);
}
/**
@@ -112,12 +112,12 @@ public class ChartFragmentTest {
// Resets last location and writes first location.
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getTimeOrDistance(), 0.01);
Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01);
// The second is a same location, just different time.
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(0.0, point.getTimeOrDistance(), 0.01);
Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01);
// The third location is a new location, and use metric.
TrackPoint trackPoint3 = TrackStubUtils.createDefaultTrackPoint();
@@ -129,7 +129,7 @@ public class ChartFragmentTest {
Location.distanceBetween(trackPoint2.getLatitude(), trackPoint2.getLongitude(),
trackPoint3.getLatitude(), trackPoint3.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM;
assertEquals(distance1, point.getTimeOrDistance(), 0.01);
Assert.assertEquals(distance1, point.getTimeOrDistance(), 0.01);
// The fourth location is a new location, and use metric.
TrackPoint trackPoint4 = TrackStubUtils.createDefaultTrackPoint();
@@ -140,7 +140,7 @@ public class ChartFragmentTest {
Location.distanceBetween(trackPoint3.getLatitude(), trackPoint3.getLongitude(),
trackPoint4.getLatitude(), trackPoint4.getLongitude(), results);
double distance2 = results[0] * UnitConversions.M_TO_KM;
assertEquals((distance1 + distance2), point.getTimeOrDistance(), 0.01);
Assert.assertEquals((distance1 + distance2), point.getTimeOrDistance(), 0.01);
}
/**
@@ -156,7 +156,7 @@ public class ChartFragmentTest {
// The first is a same location, just different time.
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getTimeOrDistance(), 0.01);
Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01);
// The second location is a new location, and use imperial.
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
@@ -170,7 +170,7 @@ public class ChartFragmentTest {
float[] results = new float[4];
Location.distanceBetween(trackPoint1.getLatitude(), trackPoint1.getLongitude(), trackPoint2.getLatitude(), trackPoint2.getLongitude(), results);
double distance1 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
assertEquals(distance1, point.getTimeOrDistance(), 0.01);
Assert.assertEquals(distance1, point.getTimeOrDistance(), 0.01);
// The third location is a new location, and use imperial.
TrackPoint trackPoint3 = TrackStubUtils.createDefaultTrackPoint();
@@ -183,7 +183,7 @@ public class ChartFragmentTest {
*/
Location.distanceBetween(trackPoint2.getLatitude(), trackPoint2.getLongitude(), trackPoint3.getLatitude(), trackPoint3.getLongitude(), results);
double distance2 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI;
assertEquals(distance1 + distance2, point.getTimeOrDistance(), 0.01);
Assert.assertEquals(distance1 + distance2, point.getTimeOrDistance(), 0.01);
}
/**
@@ -194,12 +194,12 @@ public class ChartFragmentTest {
chartFragment.setChartByDistance(false);
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getTimeOrDistance(), 0.01);
Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01);
long timeSpan = 222;
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
trackPoint2.setTime(trackPoint1.getTime() + timeSpan);
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals((double) timeSpan, point.getTimeOrDistance(), 0.01);
Assert.assertEquals((double) timeSpan, point.getTimeOrDistance(), 0.01);
}
/**
@@ -214,7 +214,7 @@ public class ChartFragmentTest {
* Then only one value INITIAL_ALTITUDE in buffer.
*/
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point.getElevation(), 0.01);
Assert.assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point.getElevation(), 0.01);
/*
* Send another value to buffer, now there are two values, INITIAL_ALTITUDE and INITIAL_ALTITUDE * 2.
@@ -222,7 +222,7 @@ public class ChartFragmentTest {
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
trackPoint2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2);
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0, point.getElevation(), 0.01);
Assert.assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0, point.getElevation(), 0.01);
}
/**
@@ -239,7 +239,7 @@ public class ChartFragmentTest {
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(128.5f);
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getSpeed(), 0.01);
Assert.assertEquals(0.0, point.getSpeed(), 0.01);
/*
* Tests the logic when both metricUnits and reportSpeed are true.
@@ -253,7 +253,7 @@ public class ChartFragmentTest {
trackPoint2.setTime(trackPoint1.getTime() + 222);
trackPoint2.setSpeed(130);
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(130.0 * UnitConversions.MPS_TO_KMH, point.getSpeed(), 0.01);
Assert.assertEquals(130.0 * UnitConversions.MPS_TO_KMH, point.getSpeed(), 0.01);
}
/**
@@ -267,7 +267,7 @@ public class ChartFragmentTest {
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(100.0f);
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getSpeed(), 0.01);
Assert.assertEquals(0.0, point.getSpeed(), 0.01);
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
@@ -278,7 +278,7 @@ public class ChartFragmentTest {
trackPoint2.setTime(trackPoint2.getTime() + 222);
trackPoint2.setSpeed(102);
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(102.0 * UnitConversions.MPS_TO_KMH * UnitConversions.KM_TO_MI, point.getSpeed(), 0.01);
Assert.assertEquals(102.0 * UnitConversions.MPS_TO_KMH * UnitConversions.KM_TO_MI, point.getSpeed(), 0.01);
}
/**
@@ -292,7 +292,7 @@ public class ChartFragmentTest {
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(100.0f);
ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getSpeed(), 0.01);
Assert.assertEquals(0.0, point.getSpeed(), 0.01);
TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint();
@@ -303,7 +303,7 @@ public class ChartFragmentTest {
trackPoint2.setTime(trackPoint2.getTime() + 222);
trackPoint2.setSpeed(102);
point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MPS_TO_KMH), point.getPace(), 0.01);
Assert.assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MPS_TO_KMH), point.getPace(), 0.01);
}
/**
@@ -315,6 +315,6 @@ public class ChartFragmentTest {
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
trackPoint.setSpeed(0);
ChartPoint point = chartFragment.createPendingPoint(trackPoint);
assertEquals(0.0, point.getPace(), 0.01);
Assert.assertEquals(0.0, point.getPace(), 0.01);
}
}
@@ -0,0 +1,131 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks.io.file.importer;
import android.content.ContentUris;
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Assert;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import java.text.SimpleDateFormat;
import java.time.ZoneOffset;
import java.util.Locale;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.when;
/**
* Abstract class for testing file track importers.
*
* @author Jimmy Shih.
*/
public abstract class AbstractTestFileTrackImporter {
static final String TRACK_NAME_0 = "blablub";
static final String TRACK_DESCRIPTION_0 = "s'Laebe isch koi Schlotzer";
static final double TRACK_LATITUDE = 48.768364;
static final double TRACK_LONGITUDE = 9.177886;
static final double TRACK_ELEVATION = 324.0;
static final String TRACK_TIME_0 = "2010-04-22T18:21:00Z";
static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123Z";
static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123Z";
static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123Z";
static final SimpleDateFormat DATE_FORMAT_0 = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US);
static final SimpleDateFormat DATE_FORMAT_1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
static final long TRACK_POINT_ID_0 = 1;
static final long TRACK_POINT_ID_1 = 2;
static final long TRACK_POINT_ID_3 = 4;
private static final long WAYPOINT_ID_0 = 1;
private static final Uri WAYPOINT_ID_O_URI = ContentUris.appendId(WaypointsColumns.CONTENT_URI.buildUpon(), WAYPOINT_ID_0).build();
protected final Context context = ApplicationProvider.getApplicationContext();
static {
// We can't omit the timezones in the test, otherwise it'll use the local timezone and fail depending on where the test runner is.
TimeZone utc = SimpleTimeZone.getTimeZone(ZoneOffset.UTC);
DATE_FORMAT_0.setTimeZone(utc);
DATE_FORMAT_1.setTimeZone(utc);
}
@Mock
public ContentProviderUtils contentProviderUtils;
TrackPoint createTrackPoint(int index, long time) {
Location trackPoint = new Location(LocationManager.GPS_PROVIDER);
trackPoint.setLatitude(TRACK_LATITUDE + index);
trackPoint.setLongitude(TRACK_LONGITUDE + index);
trackPoint.setAltitude(TRACK_ELEVATION + index);
trackPoint.setTime(time);
return new TrackPoint(trackPoint);
}
/**
* Expects the first track point to be added.
*
* @param trackPoint the trackPoint
* @param trackId the track id
* @param trackPointId the track point id
*/
protected void expectFirstTrackPoint(TrackPoint trackPoint, Track.Id trackId, long trackPointId) {
when(contentProviderUtils.bulkInsertTrackPoint(trackPoint != null ? (TrackPoint[]) any() : (TrackPoint[]) any(), eq(trackId))).thenReturn(1);
}
/**
* Expects the track to be updated.
*
* @param trackCaptor the track
* @param lastTrack true if it is the last track in the gpx
* @param trackId the track id
*/
protected void expectTrackUpdate(ArgumentCaptor<Track> trackCaptor, boolean lastTrack, Track.Id trackId) {
contentProviderUtils.updateTrack(trackCaptor.capture());
when(contentProviderUtils.insertWaypoint(any())).thenReturn(WAYPOINT_ID_O_URI);
if (lastTrack) {
// Return null to not add waypoints
when(contentProviderUtils.getTrack(trackId)).thenReturn(null);
}
}
protected void verifyTrack(Track track, String name, String description, long time) {
Assert.assertEquals(name, track.getName());
Assert.assertEquals(description, track.getDescription());
if (time != -1L) {
Assert.assertEquals(time, track.getTrackStatistics().getStartTime_ms());
}
}
}
@@ -9,7 +9,6 @@ import androidx.test.filters.LargeTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -18,13 +17,14 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
@@ -53,7 +53,7 @@ public class ExportImportTest {
private static final String TRACK_CATEGORY = "the category";
private static final String TRACK_DESCRIPTION = "the description";
private final List<Marker> markers = new ArrayList<>();
private final List<Waypoint> waypoints = new ArrayList<>();
private final List<TrackPoint> trackPoints = new ArrayList<>();
private Track.Id importTrackId;
@@ -61,7 +61,7 @@ public class ExportImportTest {
@Before
public void setUp() {
Pair<Track, List<TrackPoint>> track = TestDataUtil.createTrack(trackId, 10);
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
track.first.setIcon(TRACK_ICON);
track.first.setCategory(TRACK_CATEGORY);
track.first.setDescription(TRACK_DESCRIPTION);
@@ -69,29 +69,28 @@ public class ExportImportTest {
contentProviderUtils.bulkInsertTrackPoint(track.second, track.first.getId());
trackPoints.clear();
trackPoints.addAll(track.second);
trackPoints.addAll(Arrays.asList(track.second));
for (int i = 0; i < 3; i++) {
Marker marker = new Marker(trackId, track.second.get(i).getLocation());
marker.setName("the marker " + i);
marker.setDescription("the marker description " + i);
marker.setCategory("the marker category" + i);
marker.setIcon("the marker icon" + i);
marker.setPhotoUrl("the photo url" + i);
contentProviderUtils.insertMarker(marker);
Waypoint waypoint = new Waypoint(track.second[i].getLocation());
waypoint.setName("the waypoint " + i);
waypoint.setDescription("the waypoint description " + i);
waypoint.setCategory("the waypoint category" + i);
waypoint.setIcon("the waypoing icon" + i);
waypoint.setPhotoUrl("the photo url" + i);
waypoint.setTrackId(trackId);
contentProviderUtils.insertWaypoint(waypoint);
markers.add(marker);
waypoints.add(waypoint);
}
assertEquals(markers.size(), contentProviderUtils.getMarkerCount(trackId));
assertEquals(waypoints.size(), contentProviderUtils.getWaypointCount(trackId));
}
@After
public void tearDown() {
contentProviderUtils.deleteTrack(context, trackId);
if (importTrackId != null) {
contentProviderUtils.deleteTrack(context, importTrackId);
}
contentProviderUtils.deleteTrack(context, importTrackId);
}
@LargeTest
@@ -130,11 +129,11 @@ public class ExportImportTest {
assertEquals(track.getIcon(), importedTrack.getIcon());
assertEquals(track.getUuid(), importedTrack.getUuid());
// 2. markers
assertMarkers();
// 2. waypoints
assertWaypoints();
// 3. trackpoints
assertTrackpoints(false, false, false, false, false);
assertTrackpoints(false, false, false);
}
@LargeTest
@@ -165,15 +164,15 @@ public class ExportImportTest {
assertEquals(track.getName(), importedTrack.getName());
assertEquals(track.getIcon(), importedTrack.getIcon());
// 2. markers
assertMarkers();
// 2. waypoints
assertWaypoints();
// 3. trackpoints
assertTrackpoints(true, true, true, true, true);
assertTrackpoints(true, true, true);
}
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
@Test
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() {
// given
PreferencesUtils.setBoolean(context, R.string.import_prevent_reimport_key, true);
@@ -196,7 +195,6 @@ public class ExportImportTest {
assertNull(importedTrack);
}
@Ignore
@LargeTest
@Test
public void kmz_only_track() {
@@ -204,7 +202,6 @@ public class ExportImportTest {
Log.e(TAG, "Test not implemented.");
}
@Ignore
@LargeTest
@Test
public void kmz_with_trackdetail() {
@@ -212,7 +209,6 @@ public class ExportImportTest {
Log.e(TAG, "Test not implemented.");
}
@Ignore
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata() {
@@ -220,7 +216,6 @@ public class ExportImportTest {
Log.e(TAG, "Test not implemented.");
}
@Ignore
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata_and_pictures() {
@@ -258,15 +253,15 @@ public class ExportImportTest {
//TODO exporting and importing a track icon is not yet supported by GpxTrackWriter.
//assertEquals(track.getIcon(), trackImported.getIcon());
// 2. markers
assertMarkers();
// 2. waypoints
assertWaypoints();
// 3. trackpoints
assertTrackpoints(false, true, true, true, true);
assertTrackpoints(false, true, true);
}
@LargeTest
@Test(expected = ImportAlreadyExistsException.class)
@Test
public void gpx_duplicate_trackUUID() {
// given
PreferencesUtils.setBoolean(context, R.string.import_prevent_reimport_key, true);
@@ -290,26 +285,26 @@ public class ExportImportTest {
assertNull(trackImported);
}
private void assertMarkers() {
assertEquals(markers.size(), contentProviderUtils.getMarkerCount(importTrackId));
private void assertWaypoints() {
assertEquals(waypoints.size(), contentProviderUtils.getWaypointCount(importTrackId));
List<Marker> importedMarkers = contentProviderUtils.getMarkers(importTrackId);
for (int i = 0; i < markers.size(); i++) {
Marker marker = markers.get(i);
Marker importMarker = importedMarkers.get(i);
assertEquals(marker.getCategory(), importMarker.getCategory());
assertEquals(marker.getDescription(), importMarker.getDescription());
// assertEquals(marker.getIcon(), importMarker.getIcon()); // TODO for KML
assertEquals(marker.getName(), importMarker.getName());
assertEquals("", importMarker.getPhotoUrl());
List<Waypoint> importedWaypoints = contentProviderUtils.getWaypoints(importTrackId);
for (int i = 0; i < waypoints.size(); i++) {
Waypoint waypoint = waypoints.get(i);
Waypoint importedWaypoint = importedWaypoints.get(i);
assertEquals(waypoint.getCategory(), importedWaypoint.getCategory());
assertEquals(waypoint.getDescription(), importedWaypoint.getDescription());
// assertEquals(waypoint.getIcon(), importedWaypoint.getIcon()); // TODO for KML
assertEquals(waypoint.getName(), importedWaypoint.getName());
assertEquals("", importedWaypoint.getPhotoUrl());
assertEquals(marker.getLocation().getLatitude(), importMarker.getLocation().getLatitude(), 0.001);
assertEquals(marker.getLocation().getLongitude(), importMarker.getLocation().getLongitude(), 0.001);
assertEquals(marker.getLocation().getAltitude(), importMarker.getLocation().getAltitude(), 0.001);
assertEquals(waypoint.getLocation().getLatitude(), importedWaypoint.getLocation().getLatitude(), 0.001);
assertEquals(waypoint.getLocation().getLongitude(), importedWaypoint.getLocation().getLongitude(), 0.001);
assertEquals(waypoint.getLocation().getAltitude(), importedWaypoint.getLocation().getAltitude(), 0.001);
}
}
private void assertTrackpoints(boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyElevationGain, boolean verifyElevationLoss) {
private void assertTrackpoints(boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence) {
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
assertEquals(trackPoints.size(), importedTrackPoints.size());
@@ -335,12 +330,6 @@ public class ExportImportTest {
if (verifyPower) {
assertEquals(trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
}
if (verifyElevationGain) {
assertEquals(trackPoint.getElevationGain(), importedTrackPoint.getElevationGain(), 0.01);
}
if (verifyElevationLoss) {
assertEquals(trackPoint.getElevationLoss(), importedTrackPoint.getElevationLoss(), 0.01);
}
}
}
}
@@ -1,93 +0,0 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.InputStream;
import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.util.StringUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* Imports a legacy kml file with a statistics marker at the beginning
*/
@RunWith(JUnit4.class)
public class LegacyKmlImportTest {
private final Context context = ApplicationProvider.getApplicationContext();
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
private Track.Id importTrackId;
@After
public void tearDown() {
if (importTrackId != null) {
contentProviderUtils.deleteTrack(context, importTrackId);
}
}
@LargeTest
@Test
public void kml_legacy_track_import() {
// given
KmlFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_track);
// when
// 1. import
importTrackId = trackImporter.importFile(inputStream);
// then
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("unknown", importedTrack.getCategory());
assertEquals("Test Track", importedTrack.getDescription());
assertEquals("2020-11-28 18:06", importedTrack.getName());
assertEquals("UNKNOWN", importedTrack.getIcon());
// 2. markers
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
// 3. trackpoints
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
assertEquals(8, importedTrackPoints.size()); // 6 trackpoints + pause and resume
// first 3 trackpoints
assertTrackpoint(importedTrackPoints.get(0), "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
assertTrackpoint(importedTrackPoints.get(1), "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
assertTrackpoint(importedTrackPoints.get(2), "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
// created pause trackpoint with time of previous valid trackpoint
assertTrackpoint(importedTrackPoints.get(3), "2020-11-28T17:06:47.888Z", 0.0, 100, 0.0);
// created resume trackpoint with time of next valid trackpoint
assertTrackpoint(importedTrackPoints.get(4), "2020-11-28T17:06:55.861Z", 0.0, 200, 0.0);
// last 3 trackpoints
assertTrackpoint(importedTrackPoints.get(5), "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
assertTrackpoint(importedTrackPoints.get(6), "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
assertTrackpoint(importedTrackPoints.get(7), "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
}
private void assertTrackpoint(final TrackPoint trackPoint, final String when, final double longitude, final double latitude, final double altitude) {
assertEquals(StringUtils.parseTime(when), trackPoint.getTime());
assertEquals(latitude, trackPoint.getLatitude(), 0.001d);
assertEquals(longitude, trackPoint.getLongitude(), 0.001d);
assertEquals(altitude, trackPoint.getAltitude(), 0.001d);
}
}
@@ -15,6 +15,7 @@
*/
package de.dennisguse.opentracks.services;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -22,8 +23,6 @@ import org.junit.runners.JUnit4;
import de.dennisguse.opentracks.services.handlers.AdaptiveLocationListenerPolicy;
import static org.junit.Assert.assertEquals;
/**
* Tests the {@link AdaptiveLocationListenerPolicy}.
*
@@ -58,17 +57,17 @@ public class AdaptiveLocationListenerPolicyTest {
@Test
public void testGetDesiredPollingInterval() {
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_BIG);
assertEquals(MAX, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
Assert.assertEquals(MAX, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_NORMAL);
// First get the half of NEW_IDLE_TIME_NORMAL, and then round it to the nearest second.
assertEquals((NEW_IDLE_TIME_NORMAL / 2 / 1000) * 1000, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
Assert.assertEquals((NEW_IDLE_TIME_NORMAL / 2 / 1000) * 1000, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_SMALL);
assertEquals(MIN, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
Assert.assertEquals(MIN, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
adaptiveLocationListenerPolicy.updateIdleTime(NEW_IDLE_TIME_LESS_THAN_MIN);
assertEquals(MIN, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
Assert.assertEquals(MIN, adaptiveLocationListenerPolicy.getDesiredPollingInterval());
}
/**
@@ -76,6 +75,6 @@ public class AdaptiveLocationListenerPolicyTest {
*/
@Test
public void testGetMinDistance() {
assertEquals(MIN_DISTANCE, adaptiveLocationListenerPolicy.getMinDistance_m());
Assert.assertEquals(MIN_DISTANCE, adaptiveLocationListenerPolicy.getMinDistance_m());
}
}
@@ -25,15 +25,16 @@ import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.FlakyTest;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -44,22 +45,15 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
* Tests for the track recording service.
*
@@ -126,22 +120,22 @@ public class TrackRecordingServiceTest {
@Test
public void testStartable() throws TimeoutException {
mServiceRule.startService(createStartIntent(context));
assertNotNull(mServiceRule.bindService(createStartIntent(context)));
Assert.assertNotNull(mServiceRule.bindService(createStartIntent(context)));
}
@MediumTest
@Test
public void testBindable() throws TimeoutException {
IBinder service = mServiceRule.bindService(createStartIntent(context));
assertNotNull(service);
Assert.assertNotNull(service);
}
@MediumTest
@Test
public void testRecording_noTracks() throws TimeoutException {
public void testRecording_noTracks() throws Exception {
// given
List<Track> tracks = contentProviderUtils.getTracks();
assertTrue(tracks.isEmpty());
Assert.assertTrue(tracks.isEmpty());
// when
Intent startIntent = createStartIntent(context);
@@ -150,13 +144,13 @@ public class TrackRecordingServiceTest {
// then
// Test if we start in no-recording mode by default.
assertFalse(service.isRecording());
assertNull(service.getRecordingTrackId());
Assert.assertFalse(service.isRecording());
Assert.assertNull(service.getRecordingTrackId());
}
@MediumTest
@Test
public void testRecording_oldTracks() throws TimeoutException {
public void testRecording_oldTracks() throws Exception {
// given
createDummyTrack(trackId, -1L, false);
@@ -164,13 +158,13 @@ public class TrackRecordingServiceTest {
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
// then
assertFalse(service.isRecording());
assertNull(service.getRecordingTrackId());
Assert.assertFalse(service.isRecording());
Assert.assertNull(service.getRecordingTrackId());
}
@MediumTest
@Test
public void testRecording_serviceRestart_whileRecording() throws TimeoutException {
public void testRecording_serviceRestart_whileRecording() throws Exception {
// given
createDummyTrack(trackId, -1L, true);
@@ -178,12 +172,12 @@ public class TrackRecordingServiceTest {
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
// then
assertTrue(service.isRecording());
Assert.assertTrue(service.isRecording());
}
@MediumTest
@Test
public void testRecording_pauseAndResume() throws TimeoutException, InterruptedException {
public void testRecording_pauseAndResume() throws Exception {
// given
createDummyTrack(trackId, -1L, true);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
@@ -193,52 +187,52 @@ public class TrackRecordingServiceTest {
service.pauseCurrentTrack();
// then
assertEquals(2, contentProviderUtils.getTrackPoints(trackId).size());
Assert.assertEquals(2, contentProviderUtils.getTrackPoints(trackId).size());
//when
service.resumeTrack(trackId);
newTrackPoint(service);
// then
assertTrue(service.isRecording());
assertEquals(trackId, service.getRecordingTrackId());
Assert.assertTrue(service.isRecording());
Assert.assertEquals(trackId, service.getRecordingTrackId());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(5, trackPoints.size());
assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(2).getLatitude(), 0.01);
assertEquals(TrackPointsColumns.RESUME_LATITUDE, trackPoints.get(3).getLatitude(), 0.01);
Assert.assertEquals(5, trackPoints.size());
Assert.assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
Assert.assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(2).getLatitude(), 0.01);
Assert.assertEquals(TrackPointsColumns.RESUME_LATITUDE, trackPoints.get(3).getLatitude(), 0.01);
}
@MediumTest
@Test
public void testRecording_resumeStoppedTrack() throws TimeoutException, InterruptedException {
public void testRecording_resumeStoppedTrack() throws Exception {
// given
createDummyTrack(trackId, -1L, true);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
newTrackPoint(service);
service.endCurrentTrack();
assertEquals(1, contentProviderUtils.getTrackPoints(trackId).size());
Assert.assertEquals(1, contentProviderUtils.getTrackPoints(trackId).size());
// when
service.resumeTrack(trackId);
newTrackPoint(service);
// then
assertTrue(service.isRecording());
assertEquals(trackId, service.getRecordingTrackId());
Assert.assertTrue(service.isRecording());
Assert.assertEquals(trackId, service.getRecordingTrackId());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(4, trackPoints.size());
assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
assertEquals(TrackPointsColumns.RESUME_LATITUDE, trackPoints.get(2).getLatitude(), 0.01);
Assert.assertEquals(4, trackPoints.size());
Assert.assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
Assert.assertEquals(TrackPointsColumns.RESUME_LATITUDE, trackPoints.get(2).getLatitude(), 0.01);
}
@Ignore("Sometimes fails on CI.")
@FlakyTest(detail = "Sometimes fails on CI.")
@MediumTest
@Test
public void testRecording_orphanedRecordingTrack() throws TimeoutException {
public void testRecording_orphanedRecordingTrack() throws Exception {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
@@ -248,17 +242,17 @@ public class TrackRecordingServiceTest {
PreferencesUtils.setLong(context, R.string.recording_track_id_key, 123L);
// then
assertFalse(service.isRecording());
assertNull(service.getRecordingTrackId());
Assert.assertFalse(service.isRecording());
Assert.assertNull(service.getRecordingTrackId());
}
@MediumTest
@Test
public void testStartNewTrack_alreadyRecording() throws TimeoutException {
public void testStartNewTrack_alreadyRecording() throws Exception {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
assertTrue(service.isRecording());
Assert.assertTrue(service.isRecording());
Track.Id trackId = service.getRecordingTrackId();
@@ -266,71 +260,71 @@ public class TrackRecordingServiceTest {
Track.Id newTrackId = service.startNewTrack();
// then
assertNull(newTrackId);
Assert.assertNull(newTrackId);
assertEquals(trackId, PreferencesUtils.getRecordingTrackId(context));
assertEquals(trackId, service.getRecordingTrackId());
Assert.assertEquals(trackId, PreferencesUtils.getRecordingTrackId(context));
Assert.assertEquals(trackId, service.getRecordingTrackId());
}
@MediumTest
@Test
public void testEndCurrentTrack_noRecording() throws TimeoutException {
public void testEndCurrentTrack_noRecording() throws Exception {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
assertFalse(service.isRecording());
Assert.assertFalse(service.isRecording());
// when
// Ending the current track when there is no recording should not result in any error.
service.endCurrentTrack();
// then
assertFalse(PreferencesUtils.isRecording(context));
assertNull(service.getRecordingTrackId());
Assert.assertFalse(PreferencesUtils.isRecording(context));
Assert.assertNull(service.getRecordingTrackId());
}
@MediumTest
@Test
public void testInsertWaypointMarker_noRecordingTrack() throws TimeoutException {
public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
assertFalse(service.isRecording());
Assert.assertFalse(service.isRecording());
// when
Marker.Id markerId = service.insertMarker(null, null, null, null);
Waypoint.Id waypointId = service.insertWaypoint(null, null, null, null);
// then
assertNull(markerId);
Assert.assertNull(waypointId);
}
@MediumTest
@Test
public void testInsertWaypointMarker_validWaypoint() throws TimeoutException, InterruptedException {
public void testInsertWaypointMarker_validWaypoint() throws Exception {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
assertTrue(service.isRecording());
Assert.assertTrue(service.isRecording());
newTrackPoint(service);
Track.Id trackId = service.getRecordingTrackId();
// when
Marker.Id markerId = service.insertMarker(null, null, null, null);
Waypoint.Id waypointId = service.insertWaypoint(null, null, null, null);
// then
assertNotEquals(-1L, markerId);
Marker wpt = contentProviderUtils.getMarker(markerId);
assertEquals(context.getString(R.string.marker_icon_url), wpt.getIcon());
assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
assertEquals(trackId, wpt.getTrackId());
assertEquals(0.0, wpt.getLength(), 0.01);
assertNotNull(wpt.getLocation());
Assert.assertNotEquals(-1L, waypointId);
Waypoint wpt = contentProviderUtils.getWaypoint(waypointId);
Assert.assertEquals(context.getString(R.string.marker_waypoint_icon_url), wpt.getIcon());
Assert.assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
Assert.assertEquals(trackId, wpt.getTrackId());
Assert.assertEquals(0.0, wpt.getLength(), 0.01);
Assert.assertNotNull(wpt.getLocation());
service.endCurrentTrack();
}
private void addTrack(Track track, boolean isRecording) {
assertNotNull(track.getId());
Assert.assertTrue(track.getId().isValid());
contentProviderUtils.insertTrack(track);
assertEquals(track.getId(), contentProviderUtils.getTrack(track.getId()).getId());
Assert.assertEquals(track.getId(), contentProviderUtils.getTrack(track.getId()).getId());
PreferencesUtils.setLong(context, R.string.recording_track_id_key, isRecording ? track.getId().getId() : PreferencesUtils.RECORDING_TRACK_ID_DEFAULT);
PreferencesUtils.setBoolean(context, R.string.recording_track_paused_key, !isRecording);
}
@@ -12,6 +12,7 @@ import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
@@ -32,9 +33,6 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet;
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
/**
* Tests insert location.
* <p>
@@ -105,11 +103,11 @@ public class TrackRecordingServiceTestLocation {
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
Assert.assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(6, trackPoints.size());
assertEquals(45.0005, trackPoints.get(5).getLatitude(), 0.01);
Assert.assertEquals(6, trackPoints.size());
Assert.assertEquals(45.0005, trackPoints.get(5).getLatitude(), 0.01);
}
@MediumTest
@@ -129,11 +127,11 @@ public class TrackRecordingServiceTestLocation {
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
Assert.assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(2, trackPoints.size());
assertEquals(45.000005, trackPoints.get(1).getLatitude(), 0.01);
Assert.assertEquals(2, trackPoints.size());
Assert.assertEquals(45.000005, trackPoints.get(1).getLatitude(), 0.01);
}
// @MediumTest
@@ -171,13 +169,13 @@ public class TrackRecordingServiceTestLocation {
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
Assert.assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(3, trackPoints.size());
assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01);
assertEquals(2, trackPoints.get(1).getAccuracy(), 0.01);
assertEquals(6, trackPoints.get(2).getAccuracy(), 0.01);
Assert.assertEquals(3, trackPoints.size());
Assert.assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01);
Assert.assertEquals(2, trackPoints.get(1).getAccuracy(), 0.01);
Assert.assertEquals(6, trackPoints.get(2).getAccuracy(), 0.01);
}
@MediumTest
@@ -197,14 +195,14 @@ public class TrackRecordingServiceTestLocation {
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
Assert.assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(4, trackPoints.size());
assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01);
assertEquals(2, trackPoints.get(1).getAccuracy(), 0.01);
assertEquals(5, trackPoints.get(2).getAccuracy(), 0.01); //TODO Check why this trackPoint is inserted.
assertEquals(6, trackPoints.get(3).getAccuracy(), 0.01);
Assert.assertEquals(4, trackPoints.size());
Assert.assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01);
Assert.assertEquals(2, trackPoints.get(1).getAccuracy(), 0.01);
Assert.assertEquals(5, trackPoints.get(2).getAccuracy(), 0.01); //TODO Check why this trackPoint is inserted.
Assert.assertEquals(6, trackPoints.get(3).getAccuracy(), 0.01);
}
@@ -240,16 +238,16 @@ public class TrackRecordingServiceTestLocation {
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
Assert.assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(6, trackPoints.size());
assertEquals(0, trackPoints.get(0).getAccuracy(), 0.01);
assertEquals(1, trackPoints.get(1).getAccuracy(), 0.01);
assertEquals(2, trackPoints.get(2).getAccuracy(), 0.01);
assertEquals(3, trackPoints.get(3).getAccuracy(), 0.01);
assertEquals(4, trackPoints.get(4).getAccuracy(), 0.01);
assertEquals(5, trackPoints.get(5).getAccuracy(), 0.01);
Assert.assertEquals(6, trackPoints.size());
Assert.assertEquals(0, trackPoints.get(0).getAccuracy(), 0.01);
Assert.assertEquals(1, trackPoints.get(1).getAccuracy(), 0.01);
Assert.assertEquals(2, trackPoints.get(2).getAccuracy(), 0.01);
Assert.assertEquals(3, trackPoints.get(3).getAccuracy(), 0.01);
Assert.assertEquals(4, trackPoints.get(4).getAccuracy(), 0.01);
Assert.assertEquals(5, trackPoints.get(5).getAccuracy(), 0.01);
}
@MediumTest
@@ -268,16 +266,16 @@ public class TrackRecordingServiceTestLocation {
service.endCurrentTrack();
// then
assertFalse(service.isRecording());
Assert.assertFalse(service.isRecording());
List<TrackPoint> trackPoints = contentProviderUtils.getTrackPoints(trackId);
assertEquals(7, trackPoints.size());
assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01);
assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
assertEquals(2, trackPoints.get(2).getAccuracy(), 0.01);
assertEquals(3, trackPoints.get(3).getAccuracy(), 0.01);
assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(4).getLatitude(), 0.01);
assertEquals(4, trackPoints.get(5).getAccuracy(), 0.01);
assertEquals(5, trackPoints.get(6).getAccuracy(), 0.01);
Assert.assertEquals(7, trackPoints.size());
Assert.assertEquals(1, trackPoints.get(0).getAccuracy(), 0.01);
Assert.assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(1).getLatitude(), 0.01);
Assert.assertEquals(2, trackPoints.get(2).getAccuracy(), 0.01);
Assert.assertEquals(3, trackPoints.get(3).getAccuracy(), 0.01);
Assert.assertEquals(TrackPointsColumns.PAUSE_LATITUDE, trackPoints.get(4).getLatitude(), 0.01);
Assert.assertEquals(4, trackPoints.get(5).getAccuracy(), 0.01);
Assert.assertEquals(5, trackPoints.get(6).getAccuracy(), 0.01);
}
}
@@ -13,6 +13,7 @@ import androidx.test.rule.ServiceTestRule;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
@@ -31,12 +32,6 @@ import de.dennisguse.opentracks.content.provider.CustomContentProvider;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
* Tests for the track recording service, which require a {@link Looper}.
*
@@ -193,23 +188,23 @@ public class TrackRecordingServiceTestLooper {
@Test
public void testIntegration_completeRecordingSession() throws TimeoutException {
List<Track> tracks = contentProviderUtils.getTracks();
assertTrue(tracks.isEmpty());
Assert.assertTrue(tracks.isEmpty());
fullRecordingSession();
}
private void fullRecordingSession() throws TimeoutException {
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(TrackRecordingServiceTest.createStartIntent(context)));
assertFalse(service.isRecording());
Assert.assertFalse(service.isRecording());
// Start a track.
Track.Id trackId = service.startNewTrack();
assertNotNull(trackId);
assertTrue(service.isRecording());
Assert.assertTrue(trackId.isValid());
Assert.assertTrue(service.isRecording());
Track track = contentProviderUtils.getTrack(trackId);
assertNotNull(track);
assertEquals(trackId, track.getId());
assertEquals(trackId, PreferencesUtils.getRecordingTrackId(context));
assertEquals(trackId, service.getRecordingTrackId());
Assert.assertNotNull(track);
Assert.assertEquals(trackId, track.getId());
Assert.assertEquals(trackId, PreferencesUtils.getRecordingTrackId(context));
Assert.assertEquals(trackId, service.getRecordingTrackId());
// Insert a few points, markers and statistics.
long startTime = System.currentTimeMillis();
@@ -226,20 +221,20 @@ public class TrackRecordingServiceTestLooper {
service.newTrackPoint(trackPoint, prefAccuracy);
if (i % 7 == 0) {
service.insertMarker(null, null, null, null);
service.insertWaypoint(null, null, null, null);
}
}
// Stop the track. Validate if it has correct data.
service.endCurrentTrack();
assertFalse(service.isRecording());
assertNull(service.getRecordingTrackId());
Assert.assertFalse(service.isRecording());
Assert.assertNull(service.getRecordingTrackId());
track = contentProviderUtils.getTrack(trackId);
assertNotNull(track);
assertEquals(trackId, track.getId());
Assert.assertNotNull(track);
Assert.assertEquals(trackId, track.getId());
TrackStatistics trackStatistics = track.getTrackStatistics();
assertNotNull(trackStatistics);
assertTrue(trackStatistics.getStartTime_ms() > 0);
assertTrue(trackStatistics.getStopTime_ms() >= trackStatistics.getStartTime_ms());
Assert.assertNotNull(trackStatistics);
Assert.assertTrue(trackStatistics.getStartTime_ms() > 0);
Assert.assertTrue(trackStatistics.getStopTime_ms() >= trackStatistics.getStartTime_ms());
}
}
@@ -1,50 +0,0 @@
package de.dennisguse.opentracks.services.sensors;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* NOTE: Test data is completely artificial.
*/
public class ElevationSumManagerTest {
private final ElevationSumManager elevationSumManager = new ElevationSumManager();
private static void addSensorValue(ElevationSumManager elevationSumManager, float[] values) {
for (float f : values) {
elevationSumManager.onSensorValueChanged(f);
}
}
@Before
public void setUp() {
elevationSumManager.reset();
}
@Test
public void getElevationGainLoss_downhill() {
// given
elevationSumManager.setConnected(true);
// then
addSensorValue(elevationSumManager, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1015.14f, 1015.15f});
// then
Assert.assertEquals(0f, elevationSumManager.getElevationGain_m(), 0.01);
Assert.assertEquals(48.0, elevationSumManager.getElevationLoss_m(), 0.01);
}
@Test
public void sensorUnavailable() {
// given
elevationSumManager.setConnected(false);
// then
elevationSumManager.onSensorValueChanged(999f);
// then
Assert.assertNull(elevationSumManager.getElevationGain_m());
Assert.assertNull(elevationSumManager.getElevationLoss_m());
}
}
@@ -23,12 +23,11 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertTrue;
/**
* Tests for {@link AnnouncementPeriodicTaskFactory}.
*
@@ -54,6 +53,6 @@ public class AnnouncementPeriodicTaskFactoryTest {
public void testCreate() {
PeriodicTaskFactory factory = new AnnouncementPeriodicTaskFactory();
PeriodicTask task = factory.create(context);
assertTrue(task instanceof AnnouncementPeriodicTask);
Assert.assertTrue(task instanceof AnnouncementPeriodicTask);
}
}
@@ -15,13 +15,12 @@
*/
package de.dennisguse.opentracks.stats;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import static org.junit.Assert.assertEquals;
/**
* Tests for {@link TrackStatistics}.
* This only tests non-trivial pieces of that class.
@@ -51,8 +50,8 @@ public class TrackStatisticsTest {
statistics2.setMovingTime(600L); // Result: 700+600
statistics.setTotalDistance(750.0);
statistics2.setTotalDistance(350.0); // Result: 750+350
statistics.setTotalElevationGain(50.0f);
statistics2.setTotalElevationGain(850.0f); // Result: 850+50
statistics.setTotalElevationGain(50.0);
statistics2.setTotalElevationGain(850.0); // Result: 850+50
statistics.setMaxSpeed(60.0); // Resulting max speed
statistics2.setMaxSpeed(30.0);
statistics.setMaxElevation(1250.0);
@@ -62,28 +61,28 @@ public class TrackStatisticsTest {
statistics.merge(statistics2);
assertEquals(1000L, statistics.getStartTime_ms());
assertEquals(4000L, statistics.getStopTime_ms());
assertEquals(2500L, statistics.getTotalTime());
assertEquals(1300L, statistics.getMovingTime());
assertEquals(1100.0, statistics.getTotalDistance(), 0.001);
assertEquals(900.0, statistics.getTotalElevationGain(), 0.001);
assertEquals(statistics.getTotalDistance() / (statistics.getMovingTime() / 1000.0), statistics.getMaxSpeed(), 0.001);
assertEquals(1200.0, statistics.getMinElevation(), 0.001);
assertEquals(3575.0, statistics.getMaxElevation(), 0.001);
Assert.assertEquals(1000L, statistics.getStartTime_ms());
Assert.assertEquals(4000L, statistics.getStopTime_ms());
Assert.assertEquals(2500L, statistics.getTotalTime());
Assert.assertEquals(1300L, statistics.getMovingTime());
Assert.assertEquals(1100.0, statistics.getTotalDistance(), 0.001);
Assert.assertEquals(900.0, statistics.getTotalElevationGain(), 0.001);
Assert.assertEquals(statistics.getTotalDistance() / (statistics.getMovingTime() / 1000.0), statistics.getMaxSpeed(), 0.001);
Assert.assertEquals(1200.0, statistics.getMinElevation(), 0.001);
Assert.assertEquals(3575.0, statistics.getMaxElevation(), 0.001);
}
@Test
public void testGetAverageSpeed() {
statistics.setTotalDistance(1000.0);
statistics.setTotalTime(50000); // in milliseconds
assertEquals(20.0, statistics.getAverageSpeed(), 0.001);
Assert.assertEquals(20.0, statistics.getAverageSpeed(), 0.001);
}
@Test
public void testGetAverageMovingSpeed() {
statistics.setTotalDistance(1000.0);
statistics.setMovingTime(20000); // in milliseconds
assertEquals(50.0, statistics.getAverageMovingSpeed(), 0.001);
Assert.assertEquals(50.0, statistics.getAverageMovingSpeed(), 0.001);
}
}
@@ -5,19 +5,11 @@ import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class AnnouncementUtilsTest {
@@ -34,49 +26,9 @@ public class AnnouncementUtilsTest {
stats.setTotalElevationGain(6000);
// when
String announcement = AnnouncementUtils.getAnnouncement(context, stats, "airplane", null);
String announcement = AnnouncementUtils.getAnnouncement(context, stats, "airplane");
// then
assertEquals("OpenTracks total distance 20.00 kilometers in 5 minutes 0 seconds at 240.0 kilometers per hour", announcement);
}
@Test
public void getAnnouncement_withInterval() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000);
stats.setTotalTime(600000);
stats.setMovingTime(300000);
stats.setMaxSpeed(100);
stats.setTotalElevationGain(6000);
List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second;
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, 1000);
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
int speedId = R.plurals.voiceSpeedKilometersPerHour;
double kmPerHour = lastInterval.getSpeed_ms() * UnitConversions.MPS_TO_KMH;
String firstPartMsg = "OpenTracks total distance 20.00 kilometers in 5 minutes 0 seconds at 240.0 kilometers per hour";
String rateMsg = " Lap speed of " + context.getResources().getQuantityString(speedId, getQuantityCount(kmPerHour), kmPerHour);
String msg = firstPartMsg + rateMsg;
// when
String announcement = AnnouncementUtils.getAnnouncement(context, stats, "airplane", lastInterval);
// then
assertEquals(msg, announcement);
}
private int getQuantityCount(double d) {
if (d == 0) {
return 0;
} else if (d == 1) {
return 1;
} else if (d == 2) {
return 2;
} else {
int count = (int) d;
return Math.max(count, 3);
}
Assert.assertEquals("OpenTracks total distance 20.00 kilometers in 5 minutes 0 seconds at 240.0 kilometers per hour", announcement);
}
}
@@ -2,13 +2,11 @@ package de.dennisguse.opentracks.util;
import android.bluetooth.BluetoothGattCharacteristic;
import org.junit.Assert;
import org.junit.Test;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class BluetoothUtilsTest {
@Test
@@ -21,7 +19,7 @@ public class BluetoothUtilsTest {
int heartRate = BluetoothUtils.parseHeartRate(characteristic);
// then
assertEquals(60, heartRate);
Assert.assertEquals(60, heartRate);
}
@Test
@@ -34,7 +32,7 @@ public class BluetoothUtilsTest {
int heartRate = BluetoothUtils.parseHeartRate(characteristic);
// then
assertEquals(257, heartRate);
Assert.assertEquals(257, heartRate);
}
@Test
@@ -46,8 +44,8 @@ public class BluetoothUtilsTest {
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
assertNull(sensor.getSpeed());
Assert.assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
Assert.assertNull(sensor.getSpeed());
}
@Test
@@ -59,8 +57,8 @@ public class BluetoothUtilsTest {
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
assertNull(sensor.getCadence());
assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
Assert.assertNull(sensor.getCadence());
Assert.assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
}
@Test
@@ -72,19 +70,7 @@ public class BluetoothUtilsTest {
SensorDataCycling.CadenceAndSpeed sensor = BluetoothUtils.parseCyclingCrankAndWheel("address", "sensorName", characteristic);
// then
assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
}
@Test
public void parseCyclingPower_power() {
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER_MEASUREMENT_CHAR_UUID, 0, 0);
characteristic.setValue(new byte[]{0, 0, 40, 0});
// when
int power_w = BluetoothUtils.parseCyclingPower(characteristic);
// then
assertEquals(40, power_w);
Assert.assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
Assert.assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
}
}
@@ -15,6 +15,7 @@
*/
package de.dennisguse.opentracks.util;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -22,8 +23,6 @@ import org.junit.runners.JUnit4;
import java.io.File;
import java.util.Arrays;
import static org.junit.Assert.assertEquals;
/**
* Tests for {@link FileUtils}.
*
@@ -38,7 +37,7 @@ public class FileUtilsTest {
@Test
public void testBuildUniqueFileName_new() {
String filename = FileUtils.buildUniqueFileName(new File("/dir"), "Filename", "ext");
assertEquals("Filename.ext", filename);
Assert.assertEquals("Filename.ext", filename);
}
/**
@@ -48,7 +47,7 @@ public class FileUtilsTest {
public void testBuildUniqueFileName_exist() {
// Expect "/default.prop" to exist on the phone/emulator
String filename = FileUtils.buildUniqueFileName(new File("/"), "default", "prop");
assertEquals("default(1).prop", filename);
Assert.assertEquals("default(1).prop", filename);
}
/**
@@ -59,13 +58,13 @@ public class FileUtilsTest {
public void testSanitizeFileName() {
String name = "Swim\10ming-^across:/the/ pacific (ocean).";
String expected = "Swim_ming-^across_the_ pacific (ocean).";
assertEquals(expected, FileUtils.sanitizeFileName(name));
Assert.assertEquals(expected, FileUtils.sanitizeFileName(name));
}
@Test
public void testSanitizeFileNameWithSuffix() {
String name = "FileName.jpeg";
assertEquals(name, FileUtils.sanitizeFileName(name));
Assert.assertEquals(name, FileUtils.sanitizeFileName(name));
}
/**
@@ -76,7 +75,7 @@ public class FileUtilsTest {
public void testSanitizeFileName_i18n() {
String name = "您好-привет";
String expected = "您好-привет";
assertEquals(expected, FileUtils.sanitizeFileName(name));
Assert.assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
@@ -87,7 +86,7 @@ public class FileUtilsTest {
public void testSanitizeFileName_special_characters() {
String name = "$%'-_@~`!(){}^#&+,;=[] ";
String expected = "$%'-_@~`!(){}^#&+,;=[] ";
assertEquals(expected, FileUtils.sanitizeFileName(name));
Assert.assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
@@ -98,7 +97,7 @@ public class FileUtilsTest {
public void testSanitizeFileName_collapse() {
String name = "hello//there";
String expected = "hello_there";
assertEquals(expected, FileUtils.sanitizeFileName(name));
Assert.assertEquals(expected, FileUtils.sanitizeFileName(name));
}
/**
@@ -115,8 +114,8 @@ public class FileUtilsTest {
String truncated = FileUtils.truncateFileName(directory, nameString, suffix);
for (int i = 0; i < truncated.length(); i++) {
assertEquals('a', truncated.charAt(i));
Assert.assertEquals('a', truncated.charAt(i));
}
assertEquals(FileUtils.MAX_FAT32_PATH_LENGTH, new File(directory, truncated + suffix).getPath().length());
Assert.assertEquals(FileUtils.MAX_FAT32_PATH_LENGTH, new File(directory, truncated + suffix).getPath().length());
}
}
@@ -6,14 +6,13 @@ import android.content.SharedPreferences;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class PreferencesUtilsTest {
@@ -30,7 +29,7 @@ public class PreferencesUtilsTest {
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(context);
// then
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL, trackFileFormat);
Assert.assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL, trackFileFormat);
}
@Test
@@ -44,7 +43,7 @@ public class PreferencesUtilsTest {
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(context);
// then
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat);
Assert.assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat);
}
@Test
@@ -58,6 +57,6 @@ public class PreferencesUtilsTest {
TrackFileFormat trackFileFormat = PreferencesUtils.getExportTrackFileFormat(context);
// then
assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat);
Assert.assertEquals(TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA, trackFileFormat);
}
}
@@ -1,49 +0,0 @@
package de.dennisguse.opentracks.util;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class PressureSensorUtilsTest {
// real data at 500Hz stationary
@Test
public void elevationChanges_none() {
// given
float[] sensorValues_hPa = new float[]{1015.6876f, 1015.699f, 1015.70905f, 1015.71075f, 1015.7224f, 1015.72f, 1015.7373f, 1015.7481f, 1015.74133f, 1015.73553f, 1015.7462f, 1015.74896f, 1015.752f, 1015.754f, 1015.7456f, 1015.73303f, 1015.7288f, 1015.7239f, 1015.7298f, 1015.7387f, 1015.72534f, 1015.72577f, 1015.72797f, 1015.71796f, 1015.7113f, 1015.7097f, 1015.7096f, 1015.7013f, 1015.7072f, 1015.71063f, 1015.7088f, 1015.7115f, 1015.713f, 1015.7121f, 1015.7062f, 1015.7062f, 1015.6994f, 1015.69617f, 1015.68945f, 1015.698f, 1015.6913f, 1015.69696f, 1015.69293f, 1015.6955f, 1015.6928f, 1015.6939f, 1015.68787f, 1015.6881f, 1015.68536f, 1015.68726f, 1015.6879f, 1015.6904f, 1015.6937f, 1015.69476f, 1015.7046f, 1015.7019f, 1015.7079f, 1015.71124f, 1015.71216f, 1015.71436f, 1015.7171f, 1015.7186f, 1015.7186f, 1015.7246f, 1015.72046f, 1015.7211f, 1015.72375f, 1015.7194f, 1015.72125f, 1015.7161f, 1015.71185f, 1015.7188f, 1015.721f, 1015.7252f, 1015.72266f, 1015.7236f, 1015.72614f, 1015.7266f, 1015.7344f, 1015.7376f, 1015.7337f, 1015.7253f, 1015.71875f, 1015.7219f, 1015.7219f, 1015.7317f, 1015.7284f, 1015.73444f, 1015.73584f, 1015.7369f, 1015.73254f, 1015.7369f, 1015.7336f, 1015.73254f, 1015.73676f, 1015.74176f, 1015.74506f, 1015.7478f, 1015.755f, 1015.7515f, 1015.7543f, 1015.7459f, 1015.7468f, 1015.7492f, 1015.74585f, 1015.74756f, 1015.74756f, 1015.75085f, 1015.754f, 1015.75336f, 1015.7568f, 1015.75165f, 1015.761f, 1015.7527f, 1015.75684f, 1015.7635f, 1015.7585f, 1015.7552f, 1015.7475f, 1015.74506f, 1015.7542f, 1015.75586f, 1015.74176f, 1015.74414f, 1015.7501f, 1015.751f, 1015.7478f, 1015.755f, 1015.75836f, 1015.7592f, 1015.75256f, 1015.7559f, 1015.76654f, 1015.76154f, 1015.766f, 1015.77014f, 1015.76904f, 1015.76013f, 1015.7674f, 1015.76514f, 1015.7674f, 1015.7635f, 1015.76404f, 1015.7607f, 1015.754f, 1015.7574f, 1015.7607f, 1015.7607f, 1015.7607f, 1015.754f, 1015.7567f, 1015.7616f, 1015.7643f, 1015.7666f, 1015.7609f, 1015.766f, 1015.76764f, 1015.755f, 1015.75757f, 1015.74927f, 1015.74927f, 1015.7426f, 1015.745f, 1015.745f, 1015.7409f, 1015.7376f, 1015.7326f, 1015.74164f, 1015.74664f, 1015.745f, 1015.7459f, 1015.7502f, 1015.7493f, 1015.7476f, 1015.7493f, 1015.751f, 1015.7601f, 1015.76434f, 1015.766f, 1015.756f, 1015.7617f, 1015.7623f, 1015.76404f, 1015.76733f, 1015.7607f, 1015.7623f, 1015.76337f, 1015.7584f, 1015.7573f, 1015.7502f, 1015.7534f, 1015.7534f, 1015.7601f, 1015.75684f, 1015.7518f, 1015.75574f, 1015.7524f, 1015.7507f, 1015.7484f, 1015.7502f, 1015.7534f, 1015.7623f, 1015.75903f, 1015.75574f, 1015.75574f, 1015.7573f, 1015.7524f, 1015.75574f, 1015.75574f, 1015.7607f, 1015.7573f, 1015.754f, 1015.74677f, 1015.754f, 1015.75574f, 1015.75903f, 1015.7573f, 1015.7551f, 1015.7573f, 1015.754f, 1015.7507f, 1015.7374f, 1015.739f, 1015.73566f, 1015.73566f, 1015.7367f, 1015.7417f, 1015.7417f, 1015.739f, 1015.74835f, 1015.74567f, 1015.7517f, 1015.74066f, 1015.7374f, 1015.739f, 1015.7417f, 1015.73065f, 1015.72894f, 1015.73505f, 1015.73334f, 1015.74005f, 1015.74005f, 1015.7367f, 1015.7434f, 1015.744f, 1015.7423f, 1015.7384f, 1015.739f, 1015.7374f, 1015.7374f, 1015.7374f, 1015.7341f, 1015.73505f, 1015.7341f, 1015.7267f, 1015.72833f, 1015.7234f, 1015.7167f, 1015.725f, 1015.7217f, 1015.7234f, 1015.73004f, 1015.7324f, 1015.7423f, 1015.74005f, 1015.73505f, 1015.73004f, 1015.739f, 1015.7417f, 1015.74567f, 1015.74066f, 1015.74146f, 1015.74146f, 1015.73816f, 1015.7274f, 1015.72906f, 1015.7274f, 1015.7341f, 1015.739f, 1015.744f, 1015.7417f, 1015.7374f, 1015.7324f, 1015.73816f, 1015.74146f, 1015.7374f, 1015.7341f, 1015.7365f, 1015.7365f, 1015.74146f, 1015.74414f, 1015.7475f, 1015.7448f, 1015.74146f, 1015.74585f, 1015.7475f, 1015.75146f, 1015.75146f, 1015.7591f, 1015.75476f, 1015.7614f, 1015.75476f, 1015.7492f, 1015.74817f, 1015.7498f, 1015.7515f, 1015.75586f, 1015.7509f, 1015.7448f, 1015.7525f, 1015.7581f, 1015.7592f};
float firstSensorValue = sensorValues_hPa[0];
// when // then
for (float v : sensorValues_hPa) {
assertNull(PressureSensorUtils.computeChanges_m(firstSensorValue, v));
}
}
// Simulate a sudden drop of elevation.
// test data
@Test
public void computeChanges_downhill() {
float[] sensorValues_hPa = new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1018f, 1018.1f, 1018.1f, 1018.1f, 1018.1f};
float firstSensorValue = sensorValues_hPa[0];
// when
float elevationGain_m = 0;
float elevationLoss_m = 0;
float lastUsedPressureValue_hPa = firstSensorValue;
for (float v : sensorValues_hPa) {
PressureSensorUtils.ElevationChange elevationChange = PressureSensorUtils.computeChanges_m(lastUsedPressureValue_hPa, v);
if (elevationChange != null) {
elevationGain_m += elevationChange.getElevationGain_m();
elevationLoss_m += elevationChange.getElevationLoss_m();
lastUsedPressureValue_hPa = elevationChange.getCurrentSensorValue_hPa();
}
}
// then
assertEquals(0f, elevationGain_m, 0.01);
assertEquals(15.0f, elevationLoss_m, 0.01);
assertEquals(1016.80f, lastUsedPressureValue_hPa, 0.01); //Expect exponential smoothing
}
}
@@ -21,15 +21,13 @@ import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* Tests for {@link StringUtils}.
*
@@ -46,7 +44,7 @@ public class StringUtilsTest {
*/
@Test
public void testFormatDateTimeIso8601() {
assertEquals("1970-01-01T00:00:12.345Z", StringUtils.formatDateTimeIso8601(12345));
Assert.assertEquals("1970-01-01T00:00:12.345Z", StringUtils.formatDateTimeIso8601(12345));
}
/**
@@ -55,19 +53,19 @@ public class StringUtilsTest {
@Test
public void testformatElapsedTime() {
// 1 second
assertEquals("00:01", StringUtils.formatElapsedTime(1000));
Assert.assertEquals("00:01", StringUtils.formatElapsedTime(1000));
// 10 seconds
assertEquals("00:10", StringUtils.formatElapsedTime(10000));
Assert.assertEquals("00:10", StringUtils.formatElapsedTime(10000));
// 1 minute
assertEquals("01:00", StringUtils.formatElapsedTime(60000));
Assert.assertEquals("01:00", StringUtils.formatElapsedTime(60000));
// 10 minutes
assertEquals("10:00", StringUtils.formatElapsedTime(600000));
Assert.assertEquals("10:00", StringUtils.formatElapsedTime(600000));
// 1 hour
assertEquals("1:00:00", StringUtils.formatElapsedTime(3600000));
Assert.assertEquals("1:00:00", StringUtils.formatElapsedTime(3600000));
// 10 hours
assertEquals("10:00:00", StringUtils.formatElapsedTime(36000000));
Assert.assertEquals("10:00:00", StringUtils.formatElapsedTime(36000000));
// 100 hours
assertEquals("100:00:00", StringUtils.formatElapsedTime(360000000));
Assert.assertEquals("100:00:00", StringUtils.formatElapsedTime(360000000));
}
/**
@@ -76,19 +74,19 @@ public class StringUtilsTest {
@Test
public void testformatElapsedTimeWithHour() {
// 1 second
assertEquals("0:00:01", StringUtils.formatElapsedTimeWithHour(1000));
Assert.assertEquals("0:00:01", StringUtils.formatElapsedTimeWithHour(1000));
// 10 seconds
assertEquals("0:00:10", StringUtils.formatElapsedTimeWithHour(10000));
Assert.assertEquals("0:00:10", StringUtils.formatElapsedTimeWithHour(10000));
// 1 minute
assertEquals("0:01:00", StringUtils.formatElapsedTimeWithHour(60000));
Assert.assertEquals("0:01:00", StringUtils.formatElapsedTimeWithHour(60000));
// 10 minutes
assertEquals("0:10:00", StringUtils.formatElapsedTimeWithHour(600000));
Assert.assertEquals("0:10:00", StringUtils.formatElapsedTimeWithHour(600000));
// 1 hour
assertEquals("1:00:00", StringUtils.formatElapsedTimeWithHour(3600000));
Assert.assertEquals("1:00:00", StringUtils.formatElapsedTimeWithHour(3600000));
// 10 hours
assertEquals("10:00:00", StringUtils.formatElapsedTimeWithHour(36000000));
Assert.assertEquals("10:00:00", StringUtils.formatElapsedTimeWithHour(36000000));
// 100 hours
assertEquals("100:00:00", StringUtils.formatElapsedTimeWithHour(360000000));
Assert.assertEquals("100:00:00", StringUtils.formatElapsedTimeWithHour(360000000));
}
/**
@@ -98,13 +96,13 @@ public class StringUtilsTest {
@Test
public void testFormatDistance() {
// A large number in metric
assertEquals("5.00 km", StringUtils.formatDistance(context, 5000, true));
Assert.assertEquals("5.00 km", StringUtils.formatDistance(context, 5000, true));
// A large number in imperial
assertEquals("3.11 mi", StringUtils.formatDistance(context, 5000, false));
Assert.assertEquals("3.11 mi", StringUtils.formatDistance(context, 5000, false));
// A small number in metric
assertEquals("100.00 m", StringUtils.formatDistance(context, 100, true));
Assert.assertEquals("100.00 m", StringUtils.formatDistance(context, 100, true));
// A small number in imperial
assertEquals("328.08 ft", StringUtils.formatDistance(context, 100, false));
Assert.assertEquals("328.08 ft", StringUtils.formatDistance(context, 100, false));
}
/**
@@ -112,8 +110,8 @@ public class StringUtilsTest {
*/
@Test
public void testFormatCData() {
assertEquals("<![CDATA[hello]]>", StringUtils.formatCData("hello"));
assertEquals("<![CDATA[hello]]]]><![CDATA[>there]]>", StringUtils.formatCData("hello]]>there"));
Assert.assertEquals("<![CDATA[hello]]>", StringUtils.formatCData("hello"));
Assert.assertEquals("<![CDATA[hello]]]]><![CDATA[>there]]>", StringUtils.formatCData("hello]]>there"));
}
/**
@@ -170,7 +168,7 @@ public class StringUtilsTest {
// This comparision tends to be flaky (difference of 1ms)
// Assert.assertEquals(calendar.getTimeInMillis(), StringUtils.parseTime(xmlDateTime));
assertTrue(calendar.getTimeInMillis() + " vs. " + StringUtils.parseTime(xmlDateTime), Math.abs(calendar.getTimeInMillis() - StringUtils.parseTime(xmlDateTime)) <= 1);
Assert.assertTrue(calendar.getTimeInMillis() + " vs. " + StringUtils.parseTime(xmlDateTime), Math.abs(calendar.getTimeInMillis() - StringUtils.parseTime(xmlDateTime)) <= 1);
}
/**
@@ -179,9 +177,9 @@ public class StringUtilsTest {
@Test
public void testGetTimeParts_positive() {
int[] parts = StringUtils.getTimeParts(61000);
assertEquals(1, parts[0]);
assertEquals(1, parts[1]);
assertEquals(0, parts[2]);
Assert.assertEquals(1, parts[0]);
Assert.assertEquals(1, parts[1]);
Assert.assertEquals(0, parts[2]);
}
/**
@@ -190,49 +188,40 @@ public class StringUtilsTest {
@Test
public void testGetTimeParts_negative() {
int[] parts = StringUtils.getTimeParts(-61000);
assertEquals(-1, parts[0]);
assertEquals(-1, parts[1]);
assertEquals(0, parts[2]);
Assert.assertEquals(-1, parts[0]);
Assert.assertEquals(-1, parts[1]);
Assert.assertEquals(0, parts[2]);
}
@Test
public void testFormatDecimal() {
assertEquals("0", StringUtils.formatDecimal(0.0, 0));
assertEquals("0", StringUtils.formatDecimal(0.1, 0));
assertEquals("1", StringUtils.formatDecimal(1.1, 0));
assertEquals("10", StringUtils.formatDecimal(10, 0));
assertEquals("10", StringUtils.formatDecimal(10.1, 0));
assertEquals("-0", StringUtils.formatDecimal(-0.1, 0));
Assert.assertEquals("0", StringUtils.formatDecimal(0.0, 0));
Assert.assertEquals("0", StringUtils.formatDecimal(0.1, 0));
Assert.assertEquals("1", StringUtils.formatDecimal(1.1, 0));
Assert.assertEquals("10", StringUtils.formatDecimal(10, 0));
Assert.assertEquals("10", StringUtils.formatDecimal(10.1, 0));
Assert.assertEquals("0", StringUtils.formatDecimal(-0.1, 0));
assertEquals("0", StringUtils.formatDecimal(0.0, 2));
assertEquals("0.1", StringUtils.formatDecimal(0.1, 2));
assertEquals("1.1", StringUtils.formatDecimal(1.1, 2));
assertEquals("10", StringUtils.formatDecimal(10, 2));
assertEquals("10.1", StringUtils.formatDecimal(10.1, 2));
assertEquals("10.11", StringUtils.formatDecimal(10.111, 2));
assertEquals("-0.1", StringUtils.formatDecimal(-0.1, 2));
Assert.assertEquals("0", StringUtils.formatDecimal(0.0, 2));
Assert.assertEquals("0.1", StringUtils.formatDecimal(0.1, 2));
Assert.assertEquals("1.1", StringUtils.formatDecimal(1.1, 2));
Assert.assertEquals("10", StringUtils.formatDecimal(10, 2));
Assert.assertEquals("10.1", StringUtils.formatDecimal(10.1, 2));
Assert.assertEquals("10.11", StringUtils.formatDecimal(10.111, 2));
Assert.assertEquals("-0.1", StringUtils.formatDecimal(-0.1, 2));
assertEquals("1", StringUtils.formatDecimal(0.99, 1));
Assert.assertEquals("1", StringUtils.formatDecimal(0.99, 1));
}
@Test
public void testGetSpeedParts() {
assertEquals("4:59", StringUtils.getSpeedParts(context, 3.34, true, false).first);
assertEquals("5:00", StringUtils.getSpeedParts(context, 3.33, true, false).first);
Assert.assertEquals("4:59", StringUtils.getSpeedParts(context, 3.34, true, false).first);
Assert.assertEquals("5:00", StringUtils.getSpeedParts(context, 3.33, true, false).first);
assertEquals("11.9", StringUtils.getSpeedParts(context, 3.31, true, true).first);
assertEquals("7.5", StringUtils.getSpeedParts(context, 3.34, false, true).first);
Assert.assertEquals("11.99", StringUtils.getSpeedParts(context, 3.33, true, true).first);
Assert.assertEquals("7.45", StringUtils.getSpeedParts(context, 3.33, false, true).first);
assertEquals("min/km", StringUtils.getSpeedParts(context, 0, true, false).second);
assertEquals("min/mi", StringUtils.getSpeedParts(context, 0, false, false).second);
}
@Test
public void testFormatSpeed() {
assertEquals("4:59 min/km", StringUtils.formatSpeed(context, 3.34, true, false));
assertEquals("8:02 min/mi", StringUtils.formatSpeed(context, 3.34, false, false));
assertEquals("12.02 km/h", StringUtils.formatSpeed(context, 3.34, true, true));
assertEquals("7.47 mph", StringUtils.formatSpeed(context, 3.34, false, true));
Assert.assertEquals("min/km", StringUtils.getSpeedParts(context, 0, true, false).second);
Assert.assertEquals("min/mi", StringUtils.getSpeedParts(context, 0, false, false).second);
}
}
@@ -21,6 +21,7 @@ import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -31,8 +32,6 @@ import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import static org.junit.Assert.assertEquals;
/**
* Tests {@link TrackNameUtils}.
*
@@ -52,7 +51,7 @@ public class TrackNameUtilsTest {
@Test
public void testTrackName_date_local() {
PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_local_value));
assertEquals(StringUtils.formatDateTime(CONTEXT, START_TIME), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
Assert.assertEquals(StringUtils.formatDateTime(CONTEXT, START_TIME), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
}
/**
@@ -62,7 +61,7 @@ public class TrackNameUtilsTest {
public void testTrackName_date_iso_8601() {
PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_date_iso_8601_value));
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(TrackNameUtils.ISO_8601_FORMAT, Locale.US);
assertEquals(simpleDateFormat.format(new Date(START_TIME)), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
Assert.assertEquals(simpleDateFormat.format(new Date(START_TIME)), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
}
/**
@@ -71,6 +70,6 @@ public class TrackNameUtilsTest {
@Test
public void testTrackName_number() {
PreferencesUtils.setString(CONTEXT, R.string.track_name_key, CONTEXT.getString(R.string.settings_recording_track_name_number_value));
assertEquals("Track " + TRACK_ID.getId(), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
Assert.assertEquals("Track " + TRACK_ID.getId(), TrackNameUtils.getTrackName(CONTEXT, TRACK_ID, START_TIME));
}
}
@@ -1,11 +1,10 @@
package de.dennisguse.opentracks.util;
import org.junit.Assert;
import org.junit.Test;
import de.dennisguse.opentracks.content.data.TrackPoint;
import static org.junit.Assert.assertEquals;
public class TrackPointUtilsTest {
@Test
@@ -19,7 +18,7 @@ public class TrackPointUtilsTest {
TrackPointUtils.fixTime(trackPoint);
// then
assertEquals(time, trackPoint.getTime());
Assert.assertEquals(time, trackPoint.getTime());
}
@Test
@@ -33,7 +32,7 @@ public class TrackPointUtilsTest {
TrackPointUtils.fixTime(trackPoint);
// then
assertEquals(time, trackPoint.getTime(), 1000);
Assert.assertEquals(time, trackPoint.getTime(), 1000);
}
@Test
@@ -47,7 +46,7 @@ public class TrackPointUtilsTest {
TrackPointUtils.fixTime(trackPoint);
// then
assertEquals(time, trackPoint.getTime());
Assert.assertEquals(time, trackPoint.getTime());
}
}
@@ -2,13 +2,12 @@ package de.dennisguse.opentracks.util;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class UUIDUtilsTest {
@@ -19,6 +18,6 @@ public class UUIDUtilsTest {
byte[] bytes = UUIDUtils.toBytes(uuid);
UUID output = UUIDUtils.fromBytes(bytes);
assertEquals(uuid, output);
Assert.assertEquals(uuid, output);
}
}
@@ -4,20 +4,16 @@ import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.TrackIconUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@RunWith(JUnit4.class)
public class AggregatedStatisticsTest {
@@ -37,7 +33,7 @@ public class AggregatedStatisticsTest {
statistics.setTotalTime(totalTime);
statistics.setMovingTime(totalTime);
statistics.setTotalDistance(totalDistance);
statistics.setTotalElevationGain(50.0f);
statistics.setTotalElevationGain(50.0);
statistics.setMaxSpeed(50.0); // Resulting max speed
statistics.setMaxElevation(1250.0);
statistics.setMinElevation(1200.0); // Resulting min elevation
@@ -59,16 +55,17 @@ public class AggregatedStatisticsTest {
Track track = createTrack(context, totalDistance, totalTime, biking);
// when
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(List.of(track));
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
aggregatedStatistics.aggregate(track);
// then
assertEquals(1, aggregatedStatistics.getCount());
assertNotNull(aggregatedStatistics.get(biking));
assertEquals(1, aggregatedStatistics.get(biking).getCountTracks());
Assert.assertEquals(1, aggregatedStatistics.getCount());
Assert.assertNotNull(aggregatedStatistics.get(biking));
Assert.assertEquals(1, aggregatedStatistics.get(biking).getCountTracks());
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(totalTime, statistics2.getMovingTime());
Assert.assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
Assert.assertEquals(totalTime, statistics2.getMovingTime());
}
@Test
@@ -81,10 +78,11 @@ public class AggregatedStatisticsTest {
Track track = createTrack(context, totalDistance, totalTime, mountainBiking);
// when
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(List.of(track));
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
aggregatedStatistics.aggregate(track);
// then
assertNotNull(aggregatedStatistics.get(mountainBiking));
Assert.assertNotNull(aggregatedStatistics.get(mountainBiking));
}
@Test
@@ -97,10 +95,11 @@ public class AggregatedStatisticsTest {
Track track = createTrack(context, totalDistance, totalTime, trailRunning);
// when
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(List.of(track));
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
aggregatedStatistics.aggregate(track);
// then
assertNotNull(aggregatedStatistics.get(trailRunning));
Assert.assertNotNull(aggregatedStatistics.get(trailRunning));
}
@Test
@@ -110,19 +109,24 @@ public class AggregatedStatisticsTest {
long totalDistance = 10000;
long totalTime = 2400000;
String biking = context.getString(R.string.activity_type_biking);
List<Track> tracks = List.of(createTrack(context, totalDistance, totalTime, biking), createTrack(context, totalDistance, totalTime, biking));
Track[] tracks = new Track[2];
for (int i = 0; i < 2; i++) {
tracks[i] = createTrack(context, totalDistance, totalTime, biking);
}
// when
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(tracks);
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
aggregatedStatistics.aggregate(tracks[0]); // biking activity 1.
aggregatedStatistics.aggregate(tracks[1]); // biking activity 2.
// then
assertEquals(1, aggregatedStatistics.getCount());
assertNotNull(aggregatedStatistics.get(biking));
assertEquals(2, aggregatedStatistics.get(biking).getCountTracks());
Assert.assertEquals(1, aggregatedStatistics.getCount());
Assert.assertNotNull(aggregatedStatistics.get(biking));
Assert.assertEquals(2, aggregatedStatistics.get(biking).getCountTracks());
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
assertEquals(totalTime * 2L, statistics2.getMovingTime());
Assert.assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
Assert.assertEquals(totalTime * 2L, statistics2.getMovingTime());
}
@Test
@@ -135,40 +139,43 @@ public class AggregatedStatisticsTest {
String running = context.getString(R.string.activity_type_running);
String walking = context.getString(R.string.activity_type_walking);
long totalTime = 2400000;
List<Track> tracks = List.of(
Track[] tracks = new Track[]{
createTrack(context, totalDistance, totalTime, biking),
createTrack(context, totalDistance, totalTime, running),
createTrack(context, totalDistance, totalTime, walking)
);
};
// when
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(tracks);
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
aggregatedStatistics.aggregate(tracks[0]); // biking activity 1.
aggregatedStatistics.aggregate(tracks[1]); // biking activity 2.
aggregatedStatistics.aggregate(tracks[2]); // biking activity 3.
// then
assertEquals(3, aggregatedStatistics.getCount());
assertNotNull(aggregatedStatistics.get(biking));
assertNotNull(aggregatedStatistics.get(running));
assertNotNull(aggregatedStatistics.get(walking));
assertEquals(1, aggregatedStatistics.get(biking).getCountTracks());
assertEquals(1, aggregatedStatistics.get(running).getCountTracks());
assertEquals(1, aggregatedStatistics.get(walking).getCountTracks());
Assert.assertEquals(3, aggregatedStatistics.getCount());
Assert.assertNotNull(aggregatedStatistics.get(biking));
Assert.assertNotNull(aggregatedStatistics.get(running));
Assert.assertNotNull(aggregatedStatistics.get(walking));
Assert.assertEquals(1, aggregatedStatistics.get(biking).getCountTracks());
Assert.assertEquals(1, aggregatedStatistics.get(running).getCountTracks());
Assert.assertEquals(1, aggregatedStatistics.get(walking).getCountTracks());
{
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(totalTime, statistics2.getMovingTime());
Assert.assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
Assert.assertEquals(totalTime, statistics2.getMovingTime());
}
{
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(totalTime, statistics2.getMovingTime());
Assert.assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
Assert.assertEquals(totalTime, statistics2.getMovingTime());
}
{
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(totalTime, statistics2.getMovingTime());
Assert.assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
Assert.assertEquals(totalTime, statistics2.getMovingTime());
}
}
@@ -182,7 +189,7 @@ public class AggregatedStatisticsTest {
String running = context.getString(R.string.activity_type_running);
String walking = context.getString(R.string.activity_type_walking);
String driving = context.getString(R.string.activity_type_driving);
List<Track> tracks = List.of(
Track[] tracks = new Track[]{
createTrack(context, totalDistance, totalTime, biking),
createTrack(context, totalDistance, totalTime, running),
createTrack(context, totalDistance, totalTime, walking),
@@ -192,62 +199,59 @@ public class AggregatedStatisticsTest {
createTrack(context, totalDistance, totalTime, biking),
createTrack(context, totalDistance, totalTime, biking),
createTrack(context, totalDistance, totalTime, biking),
createTrack(context, totalDistance, totalTime, driving)
);
createTrack(context, totalDistance, totalTime, driving),
};
// when
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(tracks);
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
for (Track track : tracks) {
aggregatedStatistics.aggregate(track);
}
// then
// 4 sports.
assertEquals(4, aggregatedStatistics.getCount());
Assert.assertEquals(4, aggregatedStatistics.getCount());
// There is a map for every sport.
assertNotNull(aggregatedStatistics.get(biking));
assertNotNull(aggregatedStatistics.get(running));
assertNotNull(aggregatedStatistics.get(walking));
assertNotNull(aggregatedStatistics.get(driving));
Assert.assertNotNull(aggregatedStatistics.get(biking));
Assert.assertNotNull(aggregatedStatistics.get(running));
Assert.assertNotNull(aggregatedStatistics.get(walking));
Assert.assertNotNull(aggregatedStatistics.get(driving));
// Number of tracks by sport.
assertEquals(5, aggregatedStatistics.get(biking).getCountTracks()); // Biking.
assertEquals(2, aggregatedStatistics.get(running).getCountTracks()); // Running.
assertEquals(2, aggregatedStatistics.get(walking).getCountTracks()); // Walking.
assertEquals(1, aggregatedStatistics.get(driving).getCountTracks()); // Driving.
Assert.assertEquals(5, aggregatedStatistics.get(biking).getCountTracks()); // Biking.
Assert.assertEquals(2, aggregatedStatistics.get(running).getCountTracks()); // Running.
Assert.assertEquals(2, aggregatedStatistics.get(walking).getCountTracks()); // Walking.
Assert.assertEquals(1, aggregatedStatistics.get(driving).getCountTracks()); // Driving.
// Biking.
{
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance * 5, statistics2.getTotalDistance(), 0);
assertEquals(totalTime * 5, statistics2.getMovingTime());
Assert.assertEquals(totalDistance * 5, statistics2.getTotalDistance(), 0);
Assert.assertEquals(totalTime * 5, statistics2.getMovingTime());
}
// Running.
{
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
assertEquals(totalTime * 2, statistics2.getMovingTime());
Assert.assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
Assert.assertEquals(totalTime * 2, statistics2.getMovingTime());
}
// Walking.
{
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
assertEquals(totalTime * 2, statistics2.getMovingTime());
Assert.assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0);
Assert.assertEquals(totalTime * 2, statistics2.getMovingTime());
}
// Driving.
{
TrackStatistics statistics2 = aggregatedStatistics.get(driving).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(totalTime, statistics2.getMovingTime());
}
// Check order
{
assertEquals(biking, aggregatedStatistics.getItem(0).getCategory());
assertEquals(driving, aggregatedStatistics.getItem(3).getCategory());
Assert.assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
Assert.assertEquals(totalTime, statistics2.getMovingTime());
}
}
}
@@ -1,138 +0,0 @@
package de.dennisguse.opentracks.viewmodels;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.List;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.UnitConversions;
import static org.junit.Assert.assertEquals;
@RunWith(JUnit4.class)
public class IntervalStatisticsTest {
private static final String TAG = IntervalStatisticsTest.class.getSimpleName();
private List<TrackPoint> buildTrackPoints(int numberOfTrackPoints) {
return TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), numberOfTrackPoints).second;
}
private TrackStatistics buildTrackStatistics(List<TrackPoint> trackPoints) {
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(trackPoints.get(0).getTime());
for (TrackPoint tp : trackPoints) {
trackStatisticsUpdater.addTrackPoint(tp, 0);
}
return trackStatisticsUpdater.getTrackStatistics();
}
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_1() {
// With 50 points and interval distance of 1000m.
// given
List<TrackPoint> trackPoints = buildTrackPoints(50);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000f;
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_2() {
// With 200 points and interval distance of 1000m.
// given
List<TrackPoint> trackPoints = buildTrackPoints(200);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000f;
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_3() {
// With 200 points and interval distance of 3000m.
// given
List<TrackPoint> trackPoints = buildTrackPoints(200);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 3000f;
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_4() {
// With 1000 points and interval distance of 3000m.
// given
List<TrackPoint> trackPoints = buildTrackPoints(1000);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 3000f;
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
/**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/
@Test
public void testBuild_5() {
// With 10000 points and interval distance of 1000m.
// given
List<TrackPoint> trackPoints = buildTrackPoints(10000);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000f;
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
private void whenAndThen(List<TrackPoint> trackPoints, TrackStatistics trackStatistics, float distanceInterval) {
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, distanceInterval);
List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList();
double totalDistance = 0d;
long totalTime = 0L;
float totalGain = 0f;
for (IntervalStatistics.Interval i : intervalList) {
totalDistance += i.getDistance_m();
totalTime += ((i.getDistance_m() / i.getSpeed_ms()) * UnitConversions.S_TO_MS);
totalGain += i.getGain_m();
}
// then
assertEquals(trackStatistics.getTotalDistance(), totalDistance, 0.01);
assertEquals(trackStatistics.getTotalTime() * UnitConversions.MS_TO_S, totalTime * UnitConversions.MS_TO_S, 0.1);
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance() / distanceInterval));
assertEquals(totalGain, trackPoints.size() * TestDataUtil.ELEVATION_GAIN, 0.1);
for (int i = 0; i < intervalList.size() - 1; i++) {
assertEquals(intervalList.get(i).getDistance_m(), distanceInterval, 0.001);
totalDistance -= intervalList.get(i).getDistance_m();
}
assertEquals(intervalList.get(intervalList.size() - 1).getDistance_m(), totalDistance, 0.01);
}
}
-173
View File
@@ -1,173 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2"
xmlns:gx="http://www.google.com/kml/ext/2.2"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1">
<Document>
<open>1</open>
<visibility>1</visibility>
<name><![CDATA[2020-11-28 18:06]]></name>
<atom:generator><![CDATA[OpenTracks (Debug)]]></atom:generator>
<Style id="track">
<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>
<IconStyle>
<scale>1.3</scale>
<Icon><href>http://earth.google.com/images/kml-icons/track-directional/track-0.png</href></Icon>
</IconStyle>
</Style>
<Style id="start"><IconStyle>
<scale>1.3</scale>
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/grn-circle.png</href></Icon>
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
</IconStyle></Style>
<Style id="end"><IconStyle>
<scale>1.3</scale>
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/red-circle.png</href></Icon>
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
</IconStyle></Style>
<Style id="waypoint"><IconStyle>
<scale>1.3</scale>
<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png</href></Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
</IconStyle></Style>
<Schema id="schema">
<gx:SimpleArrayField name="speed" type="float">
<displayName><![CDATA[Speed (m/s)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="power" type="float">
<displayName><![CDATA[Power (W)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="cadence" type="float">
<displayName><![CDATA[Cadence (rpm)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="heart_rate" type="float">
<displayName><![CDATA[Heart rate (bpm)]]></displayName>
</gx:SimpleArrayField>
</Schema>
<Folder>
<name><![CDATA[Sun, 28 Nov 2020 18:06 Markers]]></name>
<open>1</open>
<Placemark>
<name><![CDATA[Split 0]]></name>
<description><![CDATA[Total distance: 0.00 km (0.0 mi)
Total time: 00:00
Moving time: 00:00
Average speed: 0.00 km/h (0.0 mi/h)
Average moving speed: 0.00 km/h (0.0 mi/h)
Max speed: 0.00 km/h (0.0 mi/h)
Average pace: 0:00 min/km (0:00 min/mi)
Average moving pace: 0:00 min/km (0:00 min/mi)
Fastest pace: 0:00 min/km (0:00 min/mi)
Max elevation: -9223372036854775808 m (-9223372036854775808 ft)
Min elevation: 9223372036854775807 m (9223372036854775807 ft)
Elevation gain: 0 m (0 ft)
Max grade: 0 %
Min grade: 0 %
Recorded: 6/23 11:29
]]></description>
<TimeStamp><when>1970-01-01T00:00:00Z</when></TimeStamp>
<styleUrl>#waypoint</styleUrl>
<Point>
<coordinates>180.0,100.0</coordinates>
</Point>
</Placemark>
</Folder>
<Placemark>
<name><![CDATA[2020-11-28 18:06 (Start)]]></name>
<description><![CDATA[]]></description>
<TimeStamp><when>2020-11-28T17:06:22.401Z</when></TimeStamp>
<styleUrl>#start</styleUrl>
<Point>
<coordinates>1.234156,12.340097,469.286376953125</coordinates>
</Point>
</Placemark>
<Placemark>
<name><![CDATA[2020-11-28 18:06]]></name>
<description><![CDATA[Test Track]]></description>
<icon><![CDATA[UNKNOWN]]></icon>
<opentracks:trackid>2f170875-55d7-445d-b2b8-22a573c81dbc</opentracks:trackid>
<styleUrl>#track</styleUrl>
<ExtendedData>
<Data name="type"><value><![CDATA[unknown]]></value></Data>
</ExtendedData>
<gx:MultiTrack>
<altitudeMode>absolute</altitudeMode>
<gx:interpolate>1</gx:interpolate>
<gx:Track>
<when>2020-11-28T17:06:22.401Z</when>
<gx:coord>1.234156 12.340097 469.286376953125</gx:coord>
<when>2020-11-28T17:06:25.448Z</when>
<gx:coord>1.23415 12.340036 439.1626281738281</gx:coord>
<when>2020-11-28T17:06:47.888Z</when>
<gx:coord>1.23405 12.340057 421.8070983886719</gx:coord>
<ExtendedData>
<SchemaData schemaUrl="#schema">
<gx:SimpleArrayData name="speed">
<gx:value>0.5391469</gx:value>
<gx:value>0.15777911</gx:value>
<gx:value>0.0</gx:value>
</gx:SimpleArrayData>
<gx:SimpleArrayData name="elevation_gain">
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
</gx:SimpleArrayData>
</SchemaData>
</ExtendedData>
</gx:Track>
<gx:Track>
<when>2020-11-28T17:06:55.861Z</when>
<gx:coord>1.23405 12.340057 419.93902587890625</gx:coord>
<when>2020-11-28T17:06:56.905Z</when>
<gx:coord>1.23405 12.340057 419.9036560058594</gx:coord>
<when>2020-11-28T17:07:20.870Z</when>
<gx:coord>1.234046 12.340082 417.99432373046875</gx:coord>
<ExtendedData>
<SchemaData schemaUrl="#schema">
<gx:SimpleArrayData name="speed">
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
</gx:SimpleArrayData>
<gx:SimpleArrayData name="elevation_gain">
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
</gx:SimpleArrayData>
</SchemaData>
</ExtendedData>
</gx:Track>
</gx:MultiTrack>
</Placemark>
<Placemark>
<name><![CDATA[2020-11-28 18:06 (End)]]></name>
<description><![CDATA[OpenTracks (Debug)
Name: 2020-11-28 18:06
Activity type: unknown
Description: -
Total distance: 0.01 km (0.0 mi)
Total time: 01:04
Moving time: 00:00
Average speed: 0.49 km/h (0.3 mi/h)
Average moving speed: 0.00 km/h (0.0 mi/h)
Max speed: 0.00 km/h (0.0 mi/h)
Average pace: 122:30 min/km (197:08 min/mi)
Average moving pace: 0:00 min/km (0:00 min/mi)
Fastest pace: 0:00 min/km (0:00 min/mi)
Max elevation: 469 m (1540 ft)
Min elevation: 419 m (1376 ft)
Elevation gain: 0 m (0 ft)
Recorded: 11/28 18:06
]]></description>
<TimeStamp><when>2020-11-28T17:07:20.870Z</when></TimeStamp>
<styleUrl>#end</styleUrl>
<Point>
<coordinates>1.234046,12.340082,417.99432373046875</coordinates>
</Point>
</Placemark>
</Document>
</kml>
+39 -190
View File
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.dennisguse.opentracks"
android:installLocation="auto">
@@ -50,11 +51,10 @@ limitations under the License.
<application
android:name=".Startup"
android:allowBackup="false"
android:hardwareAccelerated="true"
android:hasFragileUserData="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/ThemeCustom">
<activity android:name=".AboutActivity" />
@@ -69,188 +69,44 @@ limitations under the License.
<activity android:name=".io.file.importer.DirectoryChooserActivity$ImportDirectoryChooserActivity" />
<activity android:name=".io.file.importer.ImportActivity">
<activity
android:name=".io.file.importer.ImportActivity"
android:theme="@style/ThemeCustomTransparentActivity">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/gpx+xml" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/gpx" />
</intent-filter>
<intent-filter
android:label="@string/app_name"
android:priority="50">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:scheme="data" />
<data android:host="*" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.gpx" />
<data android:pathPattern=".*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
</intent-filter>
<intent-filter
android:label="@string/app_name"
android:priority="50">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:scheme="data" />
<data android:host="*" />
<data android:pathPattern=".*\\.gpx" />
<data android:pathPattern=".*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.google-earth.kmz" />
</intent-filter>
<intent-filter
android:label="@string/app_name"
android:priority="50">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:scheme="data" />
<data android:host="*" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.kmz" />
<data android:pathPattern=".*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
</intent-filter>
<intent-filter
android:label="@string/app_name"
android:priority="50">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:scheme="data" />
<data android:host="*" />
<data android:pathPattern=".*\\.kmz" />
<data android:pathPattern=".*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data
android:scheme="content"
android:host="*"
android:pathPattern="*kmz" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.google-earth.kml+xml" />
<data
android:scheme="content"
android:host="*"
android:pathPattern="*kml" />
</intent-filter>
<intent-filter
android:label="@string/app_name"
android:priority="50">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:scheme="data" />
<data android:host="*" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.kml" />
<data android:pathPattern=".*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
</intent-filter>
<intent-filter
android:label="@string/app_name"
android:priority="50">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:scheme="data" />
<data android:host="*" />
<data android:pathPattern=".*\\.kml" />
<data android:pathPattern=".*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kml" />
<data android:mimeType="application/gpx+xml" />
<data
android:scheme="content"
android:host="*"
android:pathPattern="*gpx" />
</intent-filter>
</activity>
<activity android:name=".MarkerDetailActivity">
<intent-filter>
<action android:name="de.dennisguse.opentracks.MarkerDetails" />
<action android:name="de.dennisguse.opentracks.MarkerDetails"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
@@ -259,24 +115,16 @@ limitations under the License.
<activity
android:name=".MarkerListActivity"
android:label="@string/menu_markers"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
android:label="@string/menu_markers">
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
android:name="android.app.default_searchable"
android:value=".SearchListActivity" />
</activity>
<activity
android:name=".TrackListActivity"
android:launchMode="singleTop"
android:theme="@style/SplashTheme">
android:name=".SearchListActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
@@ -290,12 +138,23 @@ limitations under the License.
<activity android:name=".TrackEditActivity" />
<activity android:name=".TrackListActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.default_searchable"
android:value=".SearchListActivity" />
</activity>
<activity android:name=".AggregatedStatisticsActivity" />
<activity android:name=".io.file.importer.DirectoryChooserActivity$ExportDirectoryChooserActivity" />
<activity android:name=".io.file.importer.DirectoryChooserActivity$DefaultTrackExportDirectoryChooserActivity" />
<activity android:name=".io.file.exporter.ExportActivity" />
<activity
android:name=".io.file.exporter.ExportActivity"
android:theme="@style/ThemeCustomTransparentActivity" />
<activity android:name=".settings.SettingsActivity" />
@@ -335,15 +194,5 @@ limitations under the License.
android:foregroundServiceType="location|connectedDevice"
android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/recording_service" />
<service
android:name=".io.file.exporter.ExportService"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".io.file.importer.ImportService"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" />
</application>
</manifest>
@@ -2,35 +2,38 @@ package de.dennisguse.opentracks;
import android.os.Bundle;
import android.view.Menu;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.databinding.AboutBinding;
import de.dennisguse.opentracks.util.SystemUtils;
import de.dennisguse.opentracks.util.ViewUtils;
public class AboutActivity extends AbstractActivity {
private AboutBinding viewBinding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(getString(R.string.about_preference_title));
viewBinding.aboutTextDescription.setText(getString(R.string.about_description));
viewBinding.aboutTextVersionName.setText(getString(R.string.about_version_name, SystemUtils.getAppVersionName(this)));
viewBinding.aboutTextVersionCode.setText(getString(R.string.about_version_code, SystemUtils.getAppVersionCode(this)));
viewBinding.aboutAppUrl.setText(getString(R.string.about_url, getString(R.string.app_web_url)));
TextView textDescription = findViewById(R.id.about_text_description);
textDescription.setText(getString(R.string.about_description));
TextView textVersionName = findViewById(R.id.about_text_version_name);
textVersionName.setText(getString(R.string.about_version_name, SystemUtils.getAppVersionName(this)));
TextView textVersionCode = findViewById(R.id.about_text_version_code);
textVersionCode.setText(getString(R.string.about_version_code, SystemUtils.getAppVersionCode(this)));
TextView textURL = findViewById(R.id.about_app_url);
textURL.setText(getString(R.string.about_url, getString(R.string.app_web_url)));
ViewUtils.makeClickableLinks(findViewById(android.R.id.content));
}
protected View getRootView() {
viewBinding = AboutBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
protected int getLayoutResId() {
return R.layout.about;
}
@Override
@@ -17,9 +17,7 @@
package de.dennisguse.opentracks;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
@@ -37,25 +35,14 @@ public abstract class AbstractActivity extends AppCompatActivity {
// Set volume control stream for text to speech
setVolumeControlStream(TTSUtils.getTTSStream());
setContentView(getRootView());
setContentView(getLayoutResId());
Toolbar toolbar = findViewById(R.id.toolbar);
setupActionBarBack(toolbar);
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
protected abstract View getRootView();
protected void setupActionBarBack(@Nullable Toolbar toolbar) {
setSupportActionBar(toolbar);
if (toolbar != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
}
/**
* Gets the layout resource id.
*/
protected abstract int getLayoutResId();
}
@@ -30,7 +30,8 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
/**
* An abstract class for the following common tasks across
* {@link TrackListActivity}, {@link TrackRecordedActivity}, and {@link TrackRecordedActivity}:
* {@link TrackListActivity}, {@link TrackRecordedActivity}, and
* {@link SearchListActivity}:
* <p>
* - share track <br>
* - delete tracks <br>
@@ -47,7 +48,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DELETE_REQUEST_CODE) {
onTrackDeleted();
onDeleted();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
@@ -92,5 +93,5 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
/**
* Called after {@link TrackDeleteActivity} returns its result.
*/
abstract protected void onTrackDeleted();
abstract protected void onDeleted();
}
@@ -1,39 +1,37 @@
package de.dennisguse.opentracks;
import android.os.Bundle;
import android.view.View;
import android.widget.ListView;
import androidx.lifecycle.ViewModelProvider;
import de.dennisguse.opentracks.adapters.AggregatedStatisticsAdapter;
import de.dennisguse.opentracks.databinding.AggregatedStatsBinding;
import de.dennisguse.opentracks.viewmodels.AggregatedStatisticsModel;
public class AggregatedStatisticsActivity extends AbstractActivity {
private AggregatedStatsBinding viewBinding;
private ListView listView;
private AggregatedStatisticsAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewBinding.aggregatedStatsList.setEmptyView(viewBinding.aggregatedStatsEmptyView);
listView = findViewById(R.id.aggregated_stats_list);
listView.setEmptyView(findViewById(R.id.aggregated_stats_empty_view));
final AggregatedStatisticsModel viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class);
viewModel.getAggregatedStats().observe(this, aggregatedStatistics -> {
if (aggregatedStatistics != null) {
adapter = new AggregatedStatisticsAdapter(this, aggregatedStatistics);
viewBinding.aggregatedStatsList.setAdapter(adapter);
adapter = new AggregatedStatisticsAdapter(getApplicationContext(), aggregatedStatistics);
listView.setAdapter(adapter);
}
adapter.notifyDataSetChanged();
});
}
@Override
protected View getRootView() {
viewBinding = AggregatedStatsBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
protected int getLayoutResId() {
return R.layout.aggregated_stats;
}
}
@@ -1,11 +1,9 @@
package de.dennisguse.opentracks;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.databinding.HelpBinding;
import de.dennisguse.opentracks.util.ViewUtils;
public class HelpActivity extends AbstractActivity {
@@ -17,7 +15,7 @@ public class HelpActivity extends AbstractActivity {
}
@Override
protected View getRootView() {
return HelpBinding.inflate(getLayoutInflater()).getRoot();
protected int getLayoutResId() {
return R.layout.help;
}
}
@@ -19,7 +19,6 @@ package de.dennisguse.opentracks;
import android.database.Cursor;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -31,9 +30,8 @@ import androidx.viewpager.widget.ViewPager;
import java.util.ArrayList;
import java.util.List;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.MarkerDetailActivityBinding;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.fragments.MarkerDetailFragment;
@@ -50,43 +48,33 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
private static final String TAG = MarkerDetailActivity.class.getSimpleName();
private MarkerDetailActivityBinding viewBinding;
private List<Marker.Id> markerIds;
private List<Waypoint.Id> waypointIds;
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
Object bundleMarkerId = getIntent().getExtras().get(EXTRA_MARKER_ID);
Marker.Id markerId = null;
if (bundleMarkerId instanceof Marker.Id) {
markerId = (Marker.Id) bundleMarkerId;
}
if (bundleMarkerId instanceof Long) {
//Incoming Intent via Dashboard API.
markerId = new Marker.Id((Long) bundleMarkerId);
}
if (markerId == null) {
Waypoint.Id waypointId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
if (waypointId == null) {
Log.d(TAG, "invalid marker id");
finish();
return;
}
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
Marker marker = contentProviderUtils.getMarker(markerId);
Waypoint waypoint = contentProviderUtils.getWaypoint(waypointId);
markerIds = new ArrayList<>();
waypointIds = new ArrayList<>();
int markerIndex = -1;
//TODO Load only markerIds, not the whole marker
try (Cursor cursor = contentProviderUtils.getMarkerCursor(marker.getTrackId(), null, -1)) {
//TODO Load only waypointIds, not the whole waypoint
try (Cursor cursor = contentProviderUtils.getWaypointCursor(waypoint.getTrackId(), null, -1)) {
if (cursor != null && cursor.moveToFirst()) {
for (int i = 0; i < cursor.getCount(); i++) {
Marker currentMarker = contentProviderUtils.createMarker(cursor);
markerIds.add(currentMarker.getId());
if (markerId.equals(currentMarker.getId())) {
markerIndex = markerIds.size() - 1;
Waypoint currentMarker = contentProviderUtils.createWaypoint(cursor);
waypointIds.add(currentMarker.getId());
if (waypointId.equals(currentMarker.getId())) {
markerIndex = waypointIds.size() - 1;
}
cursor.moveToNext();
@@ -94,9 +82,10 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
}
}
final ViewPager viewPager = findViewById(R.id.maker_detail_activity_view_pager);
final MarkerDetailPagerAdapter markerAdapter = new MarkerDetailPagerAdapter(getSupportFragmentManager());
viewBinding.makerDetailActivityViewPager.setAdapter(markerAdapter);
viewBinding.makerDetailActivityViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
viewPager.setAdapter(markerAdapter);
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
@@ -111,17 +100,16 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
public void onPageScrollStateChanged(int state) {
}
});
viewBinding.makerDetailActivityViewPager.setCurrentItem(markerIndex == -1 ? 0 : markerIndex);
viewPager.setCurrentItem(markerIndex == -1 ? 0 : markerIndex);
}
@Override
protected View getRootView() {
viewBinding = MarkerDetailActivityBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
protected int getLayoutResId() {
return R.layout.marker_detail_activity;
}
@Override
public void onMarkerDeleted() {
public void onDeleteMarkerDone() {
runOnUiThread(this::finish);
}
@@ -139,7 +127,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
@Override
@NonNull
public Fragment getItem(int position) {
return MarkerDetailFragment.newInstance(markerIds.get(position));
return MarkerDetailFragment.newInstance(waypointIds.get(position));
}
@Nullable
@@ -150,7 +138,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
@Override
public int getCount() {
return markerIds.size();
return waypointIds.size();
}
}
}
@@ -30,19 +30,24 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.databinding.MarkerEditBinding;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.viewmodels.MarkerEditViewModel;
/**
* An activity to add/edit a marker.
@@ -54,77 +59,131 @@ public class MarkerEditActivity extends AbstractActivity {
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
private static final String BUNDLE_PHOTO_URI = "photo_uri";
private static final int CAMERA_REQUEST_CODE = 5;
private static final int GALLERY_IMG_REQUEST_CODE = 7;
private static final String TAG = MarkerEditActivity.class.getSimpleName();
private Track.Id trackId;
private Marker marker;
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
private Waypoint waypoint;
private MenuItem insertPhotoMenuItem;
private MenuItem insertGalleryImgMenuItem;
private Uri photoUri;
private boolean hasCamera;
private Uri cameraPhotoUri;
private MarkerEditViewModel viewModel;
// UI elements
private MarkerEditBinding viewBinding;
private EditText waypointName;
private AutoCompleteTextView waypointMarkerType;
private EditText waypointDescription;
private ImageView waypointPhoto;
private ImageView waypointDeletePhotoBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
Marker.Id markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
Waypoint.Id waypointId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
// Setup UI elements
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.marker_types, android.R.layout.simple_dropdown_item_1line);
viewBinding.markerEditMarkerType.setAdapter(adapter);
viewBinding.markerEditPhotoDelete.setOnClickListener(v -> {
viewModel.onPhotoDelete(viewBinding.markerEditName.getText().toString(),
viewBinding.markerEditMarkerType.getText().toString(),
viewBinding.markerEditDescription.getText().toString());
});
waypointName = findViewById(R.id.marker_edit_waypoint_name);
waypointMarkerType = findViewById(R.id.marker_edit_waypoint_marker_type);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.waypoint_types, android.R.layout.simple_dropdown_item_1line);
waypointMarkerType.setAdapter(adapter);
waypointDescription = findViewById(R.id.marker_edit_waypoint_description);
waypointPhoto = findViewById(R.id.marker_edit_waypoint_photo);
viewBinding.markerEditCancel.setOnClickListener(v -> {
viewModel.onCancel();
finish();
});
boolean isNewMarker = markerId == null;
setTitle(isNewMarker ? R.string.menu_insert_marker : R.string.menu_edit);
viewBinding.markerEditDone.setText(isNewMarker ? R.string.generic_add : R.string.generic_save);
viewBinding.markerEditDone.setOnClickListener(v -> {
viewModel.onDone(viewBinding.markerEditName.getText().toString(),
viewBinding.markerEditMarkerType.getText().toString(),
viewBinding.markerEditDescription.getText().toString());
finish();
});
viewModel = new ViewModelProvider(this).get(MarkerEditViewModel.class);
viewModel.getMarkerData(trackId, markerId).observe(this, data -> {
marker = data;
viewBinding.markerEditName.setText(marker.getName());
viewBinding.markerEditMarkerType.setText(marker.getCategory());
viewBinding.markerEditDescription.setText(marker.getDescription());
if (marker.hasPhoto()) {
setMarkerImageView(marker.getPhotoURI());
} else {
viewBinding.markerEditPhoto.setImageDrawable(null);
waypointDeletePhotoBtn = findViewById(R.id.marker_edit_waypoint_photo_delete);
waypointDeletePhotoBtn.setOnClickListener(v -> {
if (waypoint != null && waypoint.hasPhoto()) {
waypoint.setPhotoUrl(null);
}
waypointPhoto.setImageBitmap(null);
photoUri = null;
hideAndShowOptions();
});
Button cancel = findViewById(R.id.marker_edit_cancel);
cancel.setOnClickListener(v -> finish());
final boolean isNewMarker = waypointId == null;
setTitle(isNewMarker ? R.string.menu_insert_marker : R.string.menu_edit);
Button done = findViewById(R.id.marker_edit_done);
done.setText(isNewMarker ? R.string.generic_add : R.string.generic_save);
done.setOnClickListener(v -> {
if (isNewMarker) {
addMarker();
} else {
saveMarker();
}
finish();
});
if (isNewMarker) {
int nextWaypointNumber = trackId == null ? -1 : new ContentProviderUtils(this).getNextWaypointNumber(trackId);
if (nextWaypointNumber == -1) {
nextWaypointNumber = 0;
}
waypointName.setText(getString(R.string.marker_name_format, nextWaypointNumber));
waypointName.selectAll();
waypointMarkerType.setText("");
waypointDescription.setText("");
} else {
waypoint = new ContentProviderUtils(this).getWaypoint(waypointId);
if (waypoint == null) {
Log.d(TAG, "waypoint is null");
finish();
return;
}
waypointName.setText(waypoint.getName());
waypointMarkerType.setText(waypoint.getCategory());
waypointDescription.setText(waypoint.getDescription());
if (waypoint.hasPhoto()) {
photoUri = waypoint.getPhotoURI();
}
}
if (savedInstanceState != null) {
photoUri = savedInstanceState.getParcelable(BUNDLE_PHOTO_URI);
if (waypoint != null) {
waypoint.setPhotoUrl(photoUri != null ? photoUri.toString() : null);
}
}
if (photoUri != null) {
setWaypointImageView(photoUri);
}
hideAndShowOptions();
}
@Override
protected View getRootView() {
viewBinding = MarkerEditBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
protected void onStart() {
super.onStart();
trackRecordingServiceConnection.startConnection(this);
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(BUNDLE_PHOTO_URI, photoUri);
}
@Override
protected void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind(this);
}
@Override
protected int getLayoutResId() {
return R.layout.marker_edit;
}
@Override
@@ -141,17 +200,16 @@ public class MarkerEditActivity extends AbstractActivity {
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.marker_edit_insert_photo) {
createMarkerWithPicture();
return true;
switch (item.getItemId()) {
case R.id.marker_edit_insert_photo:
createWaypointWithPicture();
return true;
case R.id.marker_edit_insert_gallery_img:
createWaypointWithGalleryImage();
return true;
default:
return super.onOptionsItemSelected(item);
}
if (item.getItemId() == R.id.marker_edit_insert_gallery_img) {
createMarkerWithGalleryImage();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
@@ -161,20 +219,26 @@ public class MarkerEditActivity extends AbstractActivity {
Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show();
return;
} else if (resultCode == RESULT_OK) {
viewModel.onNewCameraPhoto(cameraPhotoUri,
viewBinding.markerEditName.getText().toString(),
viewBinding.markerEditMarkerType.getText().toString(),
viewBinding.markerEditDescription.getText().toString());
setWaypointImageView(photoUri);
}
} else if (requestCode == GALLERY_IMG_REQUEST_CODE) {
if (resultCode == RESULT_CANCELED) {
Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show();
return;
} else if (resultCode == RESULT_OK) {
viewModel.onNewGalleryPhoto(data.getData(),
viewBinding.markerEditName.getText().toString(),
viewBinding.markerEditMarkerType.getText().toString(),
viewBinding.markerEditDescription.getText().toString());
} else if (resultCode == RESULT_OK) {
Uri srcUri = data.getData();
try (ParcelFileDescriptor parcelFd = getContentResolver().openFileDescriptor(srcUri, "r")) {
FileDescriptor srcFd = parcelFd.getFileDescriptor();
File dstFile = new File(FileUtils.getImageUrl(this, getTrackId()));
FileUtils.copy(srcFd, dstFile);
photoUri = FileUtils.getUriForFile(this, dstFile);
setWaypointImageView(photoUri);
} catch(Exception e) {
Log.e(TAG, e.getMessage());
Toast.makeText(this, R.string.marker_add_canceled, Toast.LENGTH_LONG).show();
return;
}
}
}
super.onActivityResult(requestCode, resultCode, data);
@@ -182,30 +246,35 @@ public class MarkerEditActivity extends AbstractActivity {
/**
* Checks and hide/shows all buttons/options about marker photo options.
* <p>
*
* If a photo is set then one's options are shown, otherwise another ones are shown.
*/
private void hideAndShowOptions() {
boolean isPhotoSet = (marker != null && marker.hasPhoto());
boolean isPhotoSet = (waypoint != null && waypoint.hasPhoto()) || photoUri != null;
if (insertPhotoMenuItem != null && insertGalleryImgMenuItem != null) {
insertPhotoMenuItem.setVisible(!isPhotoSet);
insertGalleryImgMenuItem.setVisible(!isPhotoSet);
}
viewBinding.markerEditPhotoDelete.setVisibility(isPhotoSet ? View.VISIBLE : View.GONE);
waypointDeletePhotoBtn.setVisibility(isPhotoSet ? View.VISIBLE : View.GONE);
}
/**
* Returns the trackId; either from track or marker.
* Returns the trackId. If trackId == -1 then get it from waypoint object.
*/
private Track.Id getTrackId() {
return trackId == null ? marker.getTrackId() : trackId;
return trackId == null ? waypoint.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")) {
FileDescriptor fd = pfd.getFileDescriptor();
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd);
viewBinding.markerEditPhoto.setImageBitmap(bitmap);
waypointPhoto.setImageBitmap(bitmap);
hideAndShowOptions();
} catch (IOException e) {
Log.e(TAG, e.getMessage());
@@ -213,14 +282,31 @@ public class MarkerEditActivity extends AbstractActivity {
}
}
private void createMarkerWithPicture() {
private void createWaypointWithPicture() {
Pair<Intent, Uri> intentAndPhotoUri = IntentUtils.createTakePictureIntent(this, getTrackId());
cameraPhotoUri = intentAndPhotoUri.second;
photoUri = intentAndPhotoUri.second;
startActivityForResult(intentAndPhotoUri.first, CAMERA_REQUEST_CODE);
}
private void createMarkerWithGalleryImage() {
private void createWaypointWithGalleryImage() {
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, GALLERY_IMG_REQUEST_CODE);
}
private void addMarker() {
trackRecordingServiceConnection.addMarker(this,
waypointName.getText().toString(),
waypointMarkerType.getText().toString(),
waypointDescription.getText().toString(),
photoUri != null ? photoUri.toString() : null);
}
private void saveMarker() {
waypoint.setName(waypointName.getText().toString());
waypoint.setCategory(waypointMarkerType.getText().toString());
waypoint.setDescription(waypointDescription.getText().toString());
waypoint.setPhotoUrl(photoUri != null ? photoUri.toString() : null);
new ContentProviderUtils(this).updateWaypoint(this, waypoint);
}
}
@@ -16,7 +16,7 @@
package de.dennisguse.opentracks;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
@@ -26,24 +26,24 @@ import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
import androidx.cursoradapter.widget.ResourceCursorAdapter;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.Loader;
import de.dennisguse.opentracks.adapters.MarkerResourceCursorAdapter;
import de.dennisguse.opentracks.adapters.ScrollVisibleViews;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.data.WaypointsColumns;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.MarkerListBinding;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
@@ -67,6 +67,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
// Note that the key can be null
if (PreferencesUtils.isKey(MarkerListActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(MarkerListActivity.this);
}
@@ -79,23 +80,23 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
}
};
private Track track;
private MarkerResourceCursorAdapter resourceCursorAdapter;
private MarkerListBinding viewBinding;
private final MarkerLoaderCallback loaderCallbacks = new MarkerLoaderCallback();
private ResourceCursorAdapter resourceCursorAdapter;
// UI elements
private ListView listView;
// Callback when an item is selected in the contextual action mode
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isSingleSelection = ids.length == 1;
menu.findItem(R.id.list_context_menu_share).setVisible(false);
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection);
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
menu.findItem(R.id.list_context_menu_delete).setVisible(true);
// Set select all to the same visibility as delete since delete is the only action that can be applied to multiple markers.
/*
* Set select all to the same visibility as delete since delete is the
* only action that can be applied to multiple markers.
*/
menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll);
}
@@ -119,19 +120,57 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
track = trackId != null ? contentProviderUtils.getTrack(trackId) : null;
viewBinding.markerList.setEmptyView(viewBinding.markerListEmpty);
viewBinding.markerList.setOnItemClickListener((parent, view, position, id) -> {
resourceCursorAdapter.markerInvalid(id);
listView = findViewById(R.id.marker_list);
listView.setEmptyView(findViewById(R.id.marker_list_empty));
listView.setOnItemClickListener((parent, view, position, id) -> {
Intent intent = IntentUtils.newIntent(MarkerListActivity.this, MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, new Marker.Id(id));
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, new Waypoint.Id(id));
startActivity(intent);
});
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override
public void bindView(View view, Context context, Cursor cursor) {
int nameIndex = cursor.getColumnIndex(WaypointsColumns.NAME);
int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
int categoryIndex = cursor.getColumnIndex(WaypointsColumns.CATEGORY);
int descriptionIndex = cursor.getColumnIndex(WaypointsColumns.DESCRIPTION);
int photoUrlIndex = cursor.getColumnIndex(WaypointsColumns.PHOTOURL);
int latitudeIndex = cursor.getColumnIndex(WaypointsColumns.LATITUDE);
int longitudeIndex = cursor.getColumnIndex(WaypointsColumns.LONGITUDE);
resourceCursorAdapter = new MarkerResourceCursorAdapter(this, R.layout.list_item);
ScrollVisibleViews scrollVisibleViews = new ScrollVisibleViews(resourceCursorAdapter);
viewBinding.markerList.setOnScrollListener(scrollVisibleViews);
viewBinding.markerList.setAdapter(resourceCursorAdapter);
ActivityUtils.configureListViewContextualMenu(viewBinding.markerList, contextualActionModeCallback);
int iconId = MarkerUtils.ICON_ID;
String name = cursor.getString(nameIndex);
long time = cursor.getLong(timeIndex);
String category = cursor.getString(categoryIndex);
String description = cursor.getString(descriptionIndex);
String photoUrl = cursor.getString(photoUrlIndex);
//TODO also show latitude and longitude in list
double latitude = cursor.getDouble(latitudeIndex);
double longitude = cursor.getDouble(longitudeIndex);
ListItemUtils.setListItem(MarkerListActivity.this, view, false, true, iconId, R.string.image_marker, name, null, null, 0, time, false, category, description, photoUrl);
}
};
listView.setAdapter(resourceCursorAdapter);
ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
LoaderManager.getInstance(this).initLoader(0, null, new LoaderCallbacks<Cursor>() {
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return ContentProviderUtils.getWaypointsLoader(MarkerListActivity.this, track != null ? track.getId() : null);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
resourceCursorAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
resourceCursorAdapter.swapCursor(null);
}
});
}
@Override
@@ -145,7 +184,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
protected void onResume() {
super.onResume();
this.invalidateOptionsMenu();
loadData(getIntent());
}
@Override
@@ -155,19 +193,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
}
@Override
protected void onDestroy() {
super.onDestroy();
resourceCursorAdapter.clear();
viewBinding = null;
resourceCursorAdapter = null;
contentProviderUtils = null;
sharedPreferences = null;
}
@Override
protected View getRootView() {
viewBinding = MarkerListBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
protected int getLayoutResId() {
return R.layout.marker_list;
}
@Override
@@ -202,55 +229,44 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
/**
* Handles a context item selection.
*
* @param itemId the menu item id
* @param longMarkerIds the marker ids
* @param itemId the menu item id
* @param longWaypointIds the marker ids
* @return true if handled.
*/
private boolean handleContextItem(int itemId, long... longMarkerIds) {
Marker.Id[] markerIds = new Marker.Id[longMarkerIds.length];
for (int i = 0; i < longMarkerIds.length; i++) {
markerIds[i] = new Marker.Id(longMarkerIds[i]);
private boolean handleContextItem(int itemId, long... longWaypointIds) {
Waypoint.Id[] waypointIds = new Waypoint.Id[longWaypointIds.length];
for (int i = 0; i < longWaypointIds.length; i++) {
waypointIds[i] = new Waypoint.Id(longWaypointIds[i]);
}
if (itemId == R.id.list_context_menu_show_on_map) {
if (markerIds.length == 1) {
IntentUtils.showCoordinateOnMap(this, contentProviderUtils.getMarker(markerIds[0]));
}
return true;
Intent intent;
switch (itemId) {
case R.id.list_context_menu_show_on_map:
if (waypointIds.length == 1) {
IntentUtils.showCoordinateOnMap(this, contentProviderUtils.getWaypoint(waypointIds[0]));
}
return true;
case R.id.list_context_menu_edit:
if (waypointIds.length == 1) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, waypointIds[0]);
startActivity(intent);
}
return true;
case R.id.list_context_menu_delete:
if (waypointIds.length > 1 && waypointIds.length == listView.getCount()) {
waypointIds = null;
}
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), waypointIds);
return true;
case R.id.list_context_menu_select_all:
for (int i = 0; i < listView.getCount(); i++) {
listView.setItemChecked(i, true);
}
return false;
default:
return false;
}
if (itemId == R.id.list_context_menu_share) {
Intent intent = IntentUtils.newShareFileIntent(this, markerIds);
if (intent != null) {
intent = Intent.createChooser(intent, null);
startActivity(intent);
}
return true;
}
if (itemId == R.id.list_context_menu_edit) {
if (markerIds.length == 1) {
resourceCursorAdapter.markerInvalid(markerIds[0].getId());
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerIds[0]);
startActivity(intent);
}
return true;
}
if (itemId == R.id.list_context_menu_delete) {
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), markerIds);
return true;
}
if (itemId == R.id.list_context_menu_select_all) {
for (int i = 0; i < viewBinding.markerList.getCount(); i++) {
viewBinding.markerList.setItemChecked(i, true);
}
return false;
}
return false;
}
@Override
@@ -262,92 +278,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
}
@Override
public void onBackPressed() {
if (loaderCallbacks.getSearchQuery() != null) {
loaderCallbacks.setSearch(null);
return;
}
SearchView searchView = (SearchView) searchMenuItem.getActionView();
if (!searchView.isIconified()) {
searchView.setIconified(true);
return;
}
super.onBackPressed();
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
loadData(intent);
}
private void loadData(Intent intent) {
String searchQuery = null;
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
searchQuery = intent.getStringExtra(SearchManager.QUERY);
}
loaderCallbacks.setSearch(searchQuery);
}
@Override
public void onMarkerDeleted() {
public void onDeleteMarkerDone() {
// Do nothing
}
private class MarkerLoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> {
private String searchQuery = null;
public String getSearchQuery() {
return searchQuery;
}
public void setSearch(String searchQuery) {
this.searchQuery = searchQuery;
restart();
if (searchQuery != null) {
setTitle(searchQuery);
} else {
setTitle(R.string.menu_markers);
}
}
public void restart() {
LoaderManager.getInstance(MarkerListActivity.this).restartLoader(0, null, loaderCallbacks);
}
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
final String[] PROJECTION = new String[]{MarkerColumns._ID,
MarkerColumns.NAME, MarkerColumns.DESCRIPTION, MarkerColumns.CATEGORY,
MarkerColumns.TIME, MarkerColumns.PHOTOURL};
if (searchQuery == null) {
if (track != null) {
return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, MarkerColumns.TRACKID + "=?", new String[]{String.valueOf(track.getId().getId())}, null);
} else {
return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, null, null, null);
}
} else {
final String SEARCH_QUERY = MarkerColumns.NAME + " LIKE ? OR " +
MarkerColumns.DESCRIPTION + " LIKE ? OR " +
MarkerColumns.CATEGORY + " LIKE ?";
final String[] selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"};
return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, SEARCH_QUERY, selectionArgs, MarkerColumns.DEFAULT_SORT_ORDER + " DESC");
}
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
resourceCursorAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
resourceCursorAdapter.swapCursor(null);
}
}
}
@@ -0,0 +1,450 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.dennisguse.opentracks;
import android.app.SearchManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import de.dennisguse.opentracks.content.SearchEngine;
import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment;
import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
/**
* An activity to display a list of searchable results.
*
* @author Rodrigo Damazio
* <p>
* TODO: allow to refine searchable (present searchable in context menu)
*/
public class SearchListActivity extends AbstractListActivity implements DeleteMarkerCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
private static final String TAG = SearchListActivity.class.getSimpleName();
private static final String IS_RECORDING_FIELD = "isRecording";
private static final String IS_PAUSED_FIELD = "isPaused";
private static final String ICON_ID_FIELD = "icon";
private static final String ICON_CONTENT_DESCRIPTION_ID_FIELD = "iconContentDescription";
private static final String NAME_FIELD = "name";
private static final String TOTAL_TIME_FIELD = "totalTime";
private static final String TOTAL_DISTANCE_FIELD = "totalDistance";
private static final String MARKER_COUNT_FIELD = "markerCount";
private static final String START_TIME_FIELD = "startTime";
private static final String CATEGORY_FIELD = "category";
private static final String DESCRIPTION_FIELD = "description";
private static final String PHOTO_URL_FIELD = "photoUrl";
private static final String MARKER_LATITUDE_FIELD = "latitude";
private static final String MARKER_LONGITUDE_FIELD = "longitude";
private static final String TRACK_ID_FIELD = "trackId";
private static final String MARKER_ID_FIELD = "markerId";
private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private SearchEngine searchEngine;
private ArrayAdapter<Map<String, Object>> arrayAdapter;
private boolean metricUnits = true;
private Track.Id recordingTrackId;
private boolean recordingTrackPaused;
// Callback when an item is selected in the contextual action mode
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
boolean isSingleSelection = positions.length == 1;
boolean isSingleSelectionTrack;
if (isSingleSelection) {
Map<String, Object> item = arrayAdapter.getItem(positions[0]);
isSingleSelectionTrack = item.get(MARKER_ID_FIELD) == null;
} else {
isSingleSelectionTrack = false;
}
// Not recording, one item, item is a track
MenuItem shareMenuItem = menu.findItem(R.id.list_context_menu_share);
if (isSingleSelectionTrack) {
shareMenuItem.setVisible(!isRecording);
}
// One item, item is a marker
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection && !isSingleSelectionTrack);
// One item, can be a track or a marker
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
// One item. If track, no restriction.
menu.findItem(R.id.list_context_menu_delete).setVisible(isSingleSelection && isSingleSelectionTrack);
// Disable select all, no action is available for multiple selection
menu.findItem(R.id.list_context_menu_select_all).setVisible(false);
}
@Override
public boolean onClick(int itemId, int[] positions, long[] ids) {
return handleContextItem(itemId, positions);
}
};
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (PreferencesUtils.isKey(SearchListActivity.this, R.string.stats_units_key, key)) {
metricUnits = PreferencesUtils.isMetricUnits(SearchListActivity.this);
}
if (PreferencesUtils.isKey(SearchListActivity.this, R.string.recording_track_id_key, key)) {
recordingTrackId = PreferencesUtils.getRecordingTrackId(SearchListActivity.this);
}
if (PreferencesUtils.isKey(SearchListActivity.this, R.string.recording_track_paused_key, key)) {
recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(SearchListActivity.this);
}
if (key != null) {
runOnUiThread(() -> arrayAdapter.notifyDataSetChanged());
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection();
searchEngine = new SearchEngine(contentProviderUtils);
arrayAdapter = new ArrayAdapter<Map<String, Object>>(this, R.layout.list_item, R.id.list_item_name) {
@NonNull
@Override
public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) {
View view = convertView;
if (convertView == null) {
view = getLayoutInflater().inflate(R.layout.list_item, parent, false);
}
Map<String, Object> resultMap = getItem(position);
boolean isRecording = (boolean) resultMap.get(IS_RECORDING_FIELD);
boolean isPaused = (boolean) resultMap.get(IS_PAUSED_FIELD);
int iconId = (int) resultMap.get(ICON_ID_FIELD);
int iconContentDescriptionId = (int) resultMap.get(ICON_CONTENT_DESCRIPTION_ID_FIELD);
String name = (String) resultMap.get(NAME_FIELD);
String totalTime = (String) resultMap.get(TOTAL_TIME_FIELD);
String totalDistance = (String) resultMap.get(TOTAL_DISTANCE_FIELD);
int markerCount = (int) resultMap.get(MARKER_COUNT_FIELD);
long startTime = (long) resultMap.get(START_TIME_FIELD);
String category = (String) resultMap.get(CATEGORY_FIELD);
String description = (String) resultMap.get(DESCRIPTION_FIELD);
String photoUrl = (String) resultMap.get(PHOTO_URL_FIELD);
ListItemUtils.setListItem(SearchListActivity.this, view, isRecording, isPaused, iconId,
iconContentDescriptionId, name, totalTime, totalDistance, markerCount,
startTime, false, category, description, photoUrl);
return view;
}
};
// UI elements
ListView listView = findViewById(R.id.search_list);
listView.setAdapter(arrayAdapter);
listView.setEmptyView(findViewById(R.id.search_list_empty));
listView.setOnItemClickListener((parent, view, position, id) -> {
Map<String, Object> item = arrayAdapter.getItem(position);
Track.Id trackId = (Track.Id) item.get(TRACK_ID_FIELD);
Long markerId = (Long) item.get(MARKER_ID_FIELD);
Intent intent = IntentUtils.newIntent(SearchListActivity.this, TrackRecordedActivity.class);
if (markerId != null) {
intent = intent.putExtra(TrackRecordedActivity.EXTRA_MARKER_ID, markerId);
} else {
intent = intent.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(intent);
});
ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
handleIntent(getIntent());
}
@Override
protected void onStart() {
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
trackRecordingServiceConnection.startConnection(this);
}
@Override
protected void onResume() {
super.onResume();
arrayAdapter.notifyDataSetChanged();
}
@Override
protected void onStop() {
super.onStop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind(this);
}
@Override
protected int getLayoutResId() {
return R.layout.search_list;
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
handleIntent(intent);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH) {
return true;
}
return super.onKeyUp(keyCode, event);
}
/**
* Handles a context item selection.
*
* @param itemId the menu item id
* @param positions the positions of the selected rows
* @return true if handled.
*/
private boolean handleContextItem(int itemId, int[] positions) {
if (positions.length != 1) {
return false;
}
Map<String, Object> item = arrayAdapter.getItem(positions[0]);
Track.Id trackId = (Track.Id) item.get(TRACK_ID_FIELD);
Waypoint.Id waypointId = (Waypoint.Id) item.get(MARKER_ID_FIELD);
Intent intent;
switch (itemId) {
case R.id.list_context_menu_show_on_map:
IntentUtils.showCoordinateOnMap(this, (double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + "");
return true;
case R.id.list_context_menu_share:
intent = IntentUtils.newShareFileIntent(this, trackId);
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.list_context_menu_edit:
if (waypointId != null) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, waypointId);
} else {
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(intent);
// Close the searchable result since its content can change after edit.
finish();
return true;
case R.id.list_context_menu_delete:
if (waypointId != null) {
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), waypointId);
} else {
deleteTracks(trackId);
}
return true;
default:
return false;
}
}
/**
* Handles the intent.
*
* @param intent the intent
*/
private void handleIntent(Intent intent) {
if (!Intent.ACTION_SEARCH.equals(intent.getAction())) {
Log.e(TAG, "Invalid intent action: " + intent);
finish();
return;
}
final String textQuery = intent.getStringExtra(SearchManager.QUERY);
setTitle(textQuery);
doSearch(textQuery);
}
/**
* Do the searchable.
*
* @param textQuery the query
*/
private void doSearch(String textQuery) {
SearchQuery query = new SearchQuery(textQuery, null, null, System.currentTimeMillis());
SortedSet<ScoredResult> scoredResults = searchEngine.search(query);
final List<Map<String, Object>> displayResults = prepareResultsforDisplay(scoredResults);
// Use the UI thread to display the results
runOnUiThread(() -> {
arrayAdapter.clear();
arrayAdapter.addAll(displayResults);
});
}
/**
* Prepares the result for display.
*
* @param scoredResults a list of score results
* @return a list of result maps
*/
private List<Map<String, Object>> prepareResultsforDisplay(Collection<ScoredResult> scoredResults) {
//TODO Replace use of map<string, object>, but rather provide Track or Waypoint directly.
ArrayList<Map<String, Object>> output = new ArrayList<>(scoredResults.size());
for (ScoredResult result : scoredResults) {
if (result.track != null) {
output.add(prepareTrackForDisplay(result.track));
} else {
output.add(prepareMarkerForDisplay(result.waypoint));
}
}
return output;
}
/**
* Prepares a marker for display by filling in a result map.
*
* @param waypoint the marker
* @return the result map
*/
private Map<String, Object> prepareMarkerForDisplay(Waypoint waypoint) {
Map<String, Object> resultMap = new HashMap<>();
//TODO: It may be more appropriate to obtain the track name as a join in the retrieval phase of the searchable.
String trackName = null;
Track.Id trackId = waypoint.getTrackId();
if (!trackId.isValid()) {
Track track = contentProviderUtils.getTrack(trackId);
if (track != null) {
trackName = track.getName();
}
}
resultMap.put(IS_RECORDING_FIELD, false);
resultMap.put(IS_PAUSED_FIELD, true);
resultMap.put(ICON_ID_FIELD, MarkerUtils.ICON_ID);
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_marker);
resultMap.put(NAME_FIELD, waypoint.getName());
// Display the marker's track name in the total time field
resultMap.put(TOTAL_TIME_FIELD, trackName == null ? null : getString(R.string.search_list_marker_track_location, trackName));
resultMap.put(TOTAL_DISTANCE_FIELD, null);
resultMap.put(MARKER_COUNT_FIELD, 0);
resultMap.put(START_TIME_FIELD, waypoint.getLocation().getTime());
resultMap.put(CATEGORY_FIELD, waypoint.getCategory());
resultMap.put(DESCRIPTION_FIELD, waypoint.getDescription());
resultMap.put(PHOTO_URL_FIELD, waypoint.getPhotoUrl());
resultMap.put(TRACK_ID_FIELD, waypoint.getTrackId());
resultMap.put(MARKER_ID_FIELD, waypoint.getId());
resultMap.put(MARKER_LATITUDE_FIELD, waypoint.getLocation().getLatitude());
resultMap.put(MARKER_LONGITUDE_FIELD, waypoint.getLocation().getLongitude());
return resultMap;
}
/**
* Prepares a track for display by filling in a result map.
*
* @param track the track
* @return the result map
*/
private Map<String, Object> prepareTrackForDisplay(Track track) {
Map<String, Object> resultMap = new HashMap<>();
TrackStatistics trackStatistics = track.getTrackStatistics();
String icon = track.getIcon();
String category = icon != null && !icon.equals("") ? null : track.getCategory();
resultMap.put(IS_RECORDING_FIELD, track.getId() == recordingTrackId);
resultMap.put(IS_PAUSED_FIELD, recordingTrackPaused);
resultMap.put(ICON_ID_FIELD, TrackIconUtils.getIconDrawable(icon));
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_track);
resultMap.put(NAME_FIELD, track.getName());
resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(trackStatistics.getTotalTime()));
resultMap.put(TOTAL_DISTANCE_FIELD, StringUtils.formatDistance(this, trackStatistics.getTotalDistance(), metricUnits));
resultMap.put(MARKER_COUNT_FIELD, contentProviderUtils.getWaypointCount(track.getId()));
resultMap.put(START_TIME_FIELD, trackStatistics.getStartTime_ms());
resultMap.put(CATEGORY_FIELD, category);
resultMap.put(DESCRIPTION_FIELD, track.getDescription());
resultMap.put(PHOTO_URL_FIELD, null);
resultMap.put(TRACK_ID_FIELD, track.getId());
resultMap.put(MARKER_ID_FIELD, null);
return resultMap;
}
@Override
public void onDeleteMarkerDone() {
runOnUiThread(() -> handleIntent(getIntent()));
}
@Override
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return trackRecordingServiceConnection;
}
@Override
protected void onDeleted() {
runOnUiThread(() -> handleIntent(getIntent()));
}
}
@@ -11,6 +11,7 @@ import androidx.appcompat.app.AppCompatActivity;
import java.util.Set;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
import de.dennisguse.opentracks.content.provider.ShareContentProvider;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
@@ -30,7 +31,7 @@ public abstract class ShowOnMapProxyActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
Set<Track.Id> trackIds = IntentDashboardUtils.extractTrackIdsFromIntent(getIntent());
showTrackfileFormat(this, trackFileFormat, trackIds);
showTrackTrackfileFormat(this, trackFileFormat, trackIds);
finish();
}
@@ -41,14 +42,14 @@ public abstract class ShowOnMapProxyActivity extends AppCompatActivity {
* @param context the context
* @param trackIds the track ids
*/
private static void showTrackfileFormat(Context context, TrackFileFormat trackFileFormat, Set<Track.Id> trackIds) {
private static void showTrackTrackfileFormat(Context context, TrackFileFormat trackFileFormat, Set<Track.Id> trackIds) {
if (trackIds.size() == 0) {
return;
}
Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(TrackPointsColumns.TRACKID, trackIds.toArray(new Track.Id[0])[0]);
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackIds, "SharingTrack", trackFileFormat);
intent.setDataAndType(uriAndMime.first, uriAndMime.second);
@@ -6,7 +6,6 @@ import android.util.Log;
import java.util.Locale;
import de.dennisguse.opentracks.util.ActivityUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
/**
@@ -33,8 +32,6 @@ public class Startup extends Application {
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.
if (PreferencesUtils.isRecording(this)) {
Log.e(TAG, "Reset recordingTrackId; likely the TrackRecordingService crashed.");
@@ -18,13 +18,19 @@ package de.dennisguse.opentracks;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.drawable.TransitionDrawable;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.View.OnClickListener;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import de.dennisguse.opentracks.databinding.TrackControllerBinding;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.util.ActivityUtils;
@@ -36,132 +42,137 @@ import de.dennisguse.opentracks.util.UnitConversions;
*
* @author Jimmy Shih
*/
public class TrackController implements View.OnTouchListener {
public class TrackController {
private static final String TAG = TrackController.class.getSimpleName();
private final Activity activity;
private final TrackRecordingServiceConnection trackRecordingServiceConnection;
private final Handler handlerUpdateTotalTime = new Handler();
private final Handler handlerUpdateTotalTime;
private final View containerView;
private final TextView statusTextView;
private final TextView totalTimeTextView;
private final ImageButton recordImageButton;
private final ImageButton stopImageButton;
private final boolean alwaysShow;
private Runnable buttonDelay;
private final int buttonDelayDuration;
private final TrackControllerBinding viewBinding;
private final TransitionDrawable transition;
private ButtonDelay buttonDelay;
private boolean isRecording;
private boolean isPaused;
private long totalTime = 0;
private long totalTimeTimestamp = 0;
private boolean isResumed = false;
private final Callback callback;
private long totalTimeTimestamp = 0;
private boolean isResumed = false;
// A runnable to update the total time.
private final Runnable updateTotalTimeRunnable = new Runnable() {
public void run() {
if (isResumed && isRecording && !isPaused) {
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(System.currentTimeMillis() - totalTimeTimestamp + totalTime));
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(System.currentTimeMillis() - totalTimeTimestamp + totalTime));
handlerUpdateTotalTime.postDelayed(this, UnitConversions.ONE_SECOND_MS);
}
}
};
@SuppressLint("ClickableViewAccessibility")
TrackController(Activity activity, TrackControllerBinding viewBinding, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, Callback callback) {
TrackController(Activity activity, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, OnClickListener recordListener, OnClickListener stopListener) {
this.activity = activity;
this.viewBinding = viewBinding;
this.trackRecordingServiceConnection = trackRecordingServiceConnection;
this.alwaysShow = alwaysShow;
this.callback = callback;
handlerUpdateTotalTime = new Handler();
containerView = activity.findViewById(R.id.track_controller_container);
statusTextView = activity.findViewById(R.id.track_controller_status);
totalTimeTextView = activity.findViewById(R.id.track_controller_total_time);
buttonDelayDuration = activity.getResources().getInteger(R.integer.buttonDelayMillis);
recordImageButton = activity.findViewById(R.id.track_controller_record);
recordImageButton.setOnTouchListener((view, motionEvent) -> onRecordTouch(activity, recordListener, motionEvent));
viewBinding.trackControllerRecord.setOnTouchListener(this);
viewBinding.trackControllerRecord.setOnClickListener((view) -> {
if (buttonDelay != null || (isRecording && !isPaused)) {
return;
}
callback.recordStart();
});
viewBinding.trackControllerStop.setOnTouchListener(this);
transition = (TransitionDrawable) viewBinding.trackControllerContainer.getBackground();
stopImageButton = activity.findViewById(R.id.track_controller_stop);
stopImageButton.setOnTouchListener((view, motionEvent) -> onStopTouch(activity, stopListener, motionEvent));
}
@SuppressLint("ClickableViewAccessibility") //We do accessibility manually.
@Override
public boolean onTouch(final View view, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
return false;
}
if (buttonDelay != null && event.getAction() == MotionEvent.ACTION_UP) {
handlerUpdateTotalTime.removeCallbacks(buttonDelay);
buttonDelay = null;
view.setPressed(false);
transition.resetTransition();
showStatusSetDefaultText();
return false;
}
//Trigger button pressed animation
view.setPressed(true);
//To pause a recording
if (viewBinding.trackControllerRecord.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& isRecording && !isPaused) {
transition.startTransition(buttonDelayDuration);
ActivityUtils.vibrate(activity, 150);
viewBinding.trackControllerStatus.setText(R.string.hold_to_pause);
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
view.performClick();
callback.recordStart(); //TODO Should be recordPause();
transition.resetTransition();
ActivityUtils.vibrate(activity, 1000);
};
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
private boolean onRecordTouch(final Activity activity, final OnClickListener recordListener, final MotionEvent motionEvent) {
if (isRecording && !isPaused) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
buttonDelay = new ButtonDelay(activity, recordImageButton, R.drawable.ic_button_pause_anim, R.string.hold_to_pause, recordListener);
new Thread(buttonDelay).start();
return true;
} else if (motionEvent.getAction() == MotionEvent.ACTION_UP ) {
recordImageButton.setImageResource(R.drawable.ic_button_pause);
if (buttonDelay != null) {
buttonDelay.canceled = true;
}
return true;
}
} else if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
recordListener.onClick(null);
return true;
}
//To stop a recording
if (viewBinding.trackControllerStop.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& isRecording) {
transition.startTransition(buttonDelayDuration);
ActivityUtils.vibrate(activity, 150);
viewBinding.trackControllerStatus.setText(R.string.hold_to_stop);
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
callback.recordStop();
ActivityUtils.vibrate(activity, 1000);
transition.resetTransition();
};
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
return true;
}
return false;
}
private boolean onStopTouch(final Activity activity, final OnClickListener stopListener, final MotionEvent motionEvent) {
if (isRecording) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
buttonDelay = new ButtonDelay(activity, stopImageButton, R.drawable.ic_button_stop_anim, R.string.hold_to_stop, stopListener);
new Thread(buttonDelay).start();
return true;
} else if (motionEvent.getAction() == MotionEvent.ACTION_UP ) {
stopImageButton.setImageResource(R.drawable.ic_button_stop);
if (buttonDelay != null) {
buttonDelay.canceled = true;
}
return true;
}
}
return false;
}
private static class ButtonDelay implements Runnable {
private boolean canceled = false;
private final ImageButton imageButton;
private final Activity activity;
private final OnClickListener clickListener;
private final int delayMillis;
private final Drawable drawable;
private final int delayMessageId;
private ButtonDelay(final Activity activity, final ImageButton imageButton, final int animDrawableId, final int delayMessageId, final OnClickListener clickListener) {
this.activity = activity;
this.clickListener = clickListener;
this.delayMillis = activity.getResources().getInteger(R.integer.buttonDelayMillis);
this.imageButton = imageButton;
this.drawable = ContextCompat.getDrawable(activity, animDrawableId);
this.delayMessageId = delayMessageId;
}
@Override
public void run() {
activity.runOnUiThread(()->{
imageButton.setImageDrawable(drawable);
if (drawable instanceof AnimatedVectorDrawable) {
((AnimatedVectorDrawable) drawable).start();
}
ActivityUtils.vibrate(activity, 150);
ActivityUtils.toast(activity, delayMessageId, Toast.LENGTH_SHORT, Gravity.TOP);
});
try {
Thread.sleep(delayMillis);
} catch (InterruptedException ignored) {
}
if (!canceled) {
activity.runOnUiThread(()-> {
clickListener.onClick(null);
ActivityUtils.vibrate(activity, 1000);
});
}
}
}
public void update(boolean recording, boolean paused) {
if (!isResumed) {
return;
@@ -169,29 +180,30 @@ public class TrackController implements View.OnTouchListener {
isRecording = recording;
isPaused = paused;
boolean visible = alwaysShow || isRecording;
viewBinding.trackControllerContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
containerView.setVisibility(visible ? View.VISIBLE : View.GONE);
if (!visible) {
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
return;
}
viewBinding.trackControllerRecord.setImageResource(isRecording && !isPaused ? R.drawable.ic_button_pause : R.drawable.button_record);
viewBinding.trackControllerRecord.setContentDescription(activity.getString(isRecording && !isPaused ? R.string.image_pause : R.string.image_record));
recordImageButton.setImageResource(isRecording && !isPaused ? R.drawable.ic_button_pause : R.drawable.button_record);
recordImageButton.setContentDescription(activity.getString(isRecording && !isPaused ? R.string.image_pause : R.string.image_record));
viewBinding.trackControllerStop.setEnabled(isRecording);
stopImageButton.setImageResource(isRecording ? R.drawable.ic_button_stop : R.drawable.ic_button_stop_disabled);
stopImageButton.setEnabled(isRecording);
viewBinding.trackControllerStatus.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
statusTextView.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
viewBinding.trackControllerStatus.setTextColor(activity.getResources().getColor(isPaused ? android.R.color.white : R.color.recording_text));
showStatusSetDefaultText();
statusTextView.setTextColor(activity.getResources().getColor(isPaused ? android.R.color.white : R.color.recording_text));
statusTextView.setText(isPaused ? R.string.generic_paused : R.string.generic_recording);
}
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
viewBinding.trackControllerTotalTime.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
totalTimeTextView.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
totalTime = getTotalTime();
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
if (!isPaused) {
totalTimeTimestamp = System.currentTimeMillis();
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND_MS);
@@ -210,11 +222,11 @@ public class TrackController implements View.OnTouchListener {
}
public void hide() {
viewBinding.trackControllerContainer.setVisibility(View.GONE);
containerView.setVisibility(View.GONE);
}
public void show() {
viewBinding.trackControllerContainer.setVisibility(View.VISIBLE);
containerView.setVisibility(View.VISIBLE);
}
/**
@@ -224,14 +236,4 @@ public class TrackController implements View.OnTouchListener {
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
return trackRecordingService != null ? trackRecordingService.getTotalTime() : 0L;
}
private void showStatusSetDefaultText() {
viewBinding.trackControllerStatus.setText(isPaused ? R.string.generic_paused : R.string.generic_recording);
}
public interface Callback {
void recordStart();
void recordStop();
}
}
@@ -25,7 +25,6 @@ import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.TrackDeleteBinding;
import de.dennisguse.opentracks.util.SystemUtils;
/**
@@ -41,8 +40,6 @@ public class TrackDeleteActivity extends AbstractActivity {
private Thread deleteThread;
private TrackDeleteBinding viewBinding;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -55,7 +52,13 @@ public class TrackDeleteActivity extends AbstractActivity {
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(TrackDeleteActivity.this);
PowerManager.WakeLock wakeLock = SystemUtils.acquireWakeLock(TrackDeleteActivity.this, null);
contentProviderUtils.deleteTracks(TrackDeleteActivity.this, trackIds);
for (Track.Id id : trackIds) {
if (Thread.interrupted()) {
break;
}
contentProviderUtils.deleteTrack(TrackDeleteActivity.this, id);
}
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
if (Thread.interrupted()) {
@@ -79,19 +82,12 @@ public class TrackDeleteActivity extends AbstractActivity {
}
@Override
protected void onDestroy() {
super.onDestroy();
viewBinding = null;
}
@Override
protected View getRootView() {
viewBinding = TrackDeleteBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
protected int getLayoutResId() {
return R.layout.track_delete;
}
public void onAsyncTaskCompleted() {
viewBinding.progressbar.setVisibility(View.INVISIBLE);
findViewById(R.id.progressbar).setVisibility(View.INVISIBLE);
setResult(RESULT_OK);
finish();
}
@@ -18,14 +18,19 @@ package de.dennisguse.opentracks;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.TrackEditBinding;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -50,7 +55,10 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
private Track track;
private String iconValue;
private TrackEditBinding viewBinding;
private EditText nameEditText;
private AutoCompleteTextView activityType;
private Spinner activityTypeIcon;
private EditText description;
@Override
protected void onCreate(Bundle bundle) {
@@ -58,7 +66,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
trackRecordingServiceConnection = new TrackRecordingServiceConnection();
Track.Id trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
if (trackId == null) {
if (!trackId.isValid()) {
Log.e(TAG, "invalid trackId");
finish();
return;
@@ -67,22 +75,25 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
contentProviderUtils = new ContentProviderUtils(this);
track = contentProviderUtils.getTrack(trackId);
if (track == null) {
Log.e(TAG, "No track for " + trackId.getId());
Log.e(TAG, "No track for " + trackId);
finish();
return;
}
viewBinding.trackEditName.setText(track.getName());
nameEditText = findViewById(R.id.track_edit_name);
nameEditText.setText(track.getName());
viewBinding.trackEditActivityType.setText(track.getCategory());
activityType = findViewById(R.id.track_edit_activity_type);
activityType.setText(track.getCategory());
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
viewBinding.trackEditActivityType.setAdapter(adapter);
viewBinding.trackEditActivityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(this, (String) viewBinding.trackEditActivityType.getAdapter().getItem(position))));
viewBinding.trackEditActivityType.setOnFocusChangeListener((v, hasFocus) -> {
activityType.setAdapter(adapter);
activityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackEditActivity.this, (String) activityType.getAdapter().getItem(position))));
activityType.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackEditActivity.this, viewBinding.trackEditActivityType.getText().toString()));
TrackEditActivity.this, activityType.getText().toString()));
}
});
@@ -94,25 +105,40 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
iconValue = track.getIcon();
}
setActivityTypeIcon(iconValue);
viewBinding.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.trackEditActivityType.getText().toString()));
activityTypeIcon = findViewById(R.id.track_edit_activity_type_icon);
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(this, iconValue));
activityTypeIcon.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
}
return true;
});
activityTypeIcon.setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), activityType.getText().toString());
}
return true;
});
viewBinding.trackEditDescription.setText(track.getDescription());
description = findViewById(R.id.track_edit_description);
description.setText(track.getDescription());
viewBinding.trackEditSave.setOnClickListener(v -> {
TrackUtils.updateTrack(TrackEditActivity.this, track, viewBinding.trackEditName.getText().toString(),
viewBinding.trackEditActivityType.getText().toString(), viewBinding.trackEditDescription.getText().toString(),
Button saveButton = findViewById(R.id.track_edit_save);
saveButton.setOnClickListener(v -> {
TrackUtils.updateTrack(TrackEditActivity.this, track, nameEditText.getText().toString(),
activityType.getText().toString(), description.getText().toString(),
contentProviderUtils);
finish();
});
Button cancel = findViewById(R.id.track_edit_cancel);
if (getIntent().getBooleanExtra(EXTRA_NEW_TRACK, false)) {
setTitle(R.string.track_edit_new_track_title);
viewBinding.trackEditCancel.setVisibility(View.GONE);
cancel.setVisibility(View.GONE);
} else {
setTitle(R.string.menu_edit);
viewBinding.trackEditCancel.setOnClickListener(v -> finish());
viewBinding.trackEditCancel.setVisibility(View.VISIBLE);
cancel.setOnClickListener(v -> finish());
cancel.setVisibility(View.VISIBLE);
}
}
@@ -136,19 +162,18 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
}
@Override
protected View getRootView() {
viewBinding = TrackEditBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
protected int getLayoutResId() {
return R.layout.track_edit;
}
private void setActivityTypeIcon(String iconValue) {
this.iconValue = iconValue;
viewBinding.trackEditActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
private void setActivityTypeIcon(String value) {
iconValue = value;
TrackIconUtils.setIconSpinner(activityTypeIcon, value);
}
@Override
public void onChooseActivityTypeDone(String value) {
setActivityTypeIcon(value);
viewBinding.trackEditActivityType.setText(getString(TrackIconUtils.getIconActivityType(value)));
activityType.setText(getString(TrackIconUtils.getIconActivityType(value)));
}
}
@@ -33,20 +33,21 @@ import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ListView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.cursoradapter.widget.ResourceCursorAdapter;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.Loader;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.databinding.TrackListBinding;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
@@ -65,20 +66,35 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
*
* @author Leif Hendrik Wilden
*/
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackController.Callback {
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
private static final String TAG = TrackListActivity.class.getSimpleName();
// The following are set in onCreate
private ContentProviderUtils contentProviderUtils;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackController trackController;
private ListView listView;
private ResourceCursorAdapter resourceCursorAdapter;
private GpsStatusValue gpsStatusValue;
private TrackListBinding viewBinding;
private final LoaderCallbacks<Cursor> loaderCallbacks = new LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return ContentProviderUtils.getTracksCursorLoader(TrackListActivity.this);
}
private final TrackLoaderCallBack loaderCallbacks = new TrackLoaderCallBack();
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
resourceCursorAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
resourceCursorAdapter.swapCursor(null);
}
};
// Preferences
private boolean metricUnits = true;
@@ -96,8 +112,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
@Override
public boolean onClick(int itemId, int[] positions, long[] trackIds) {
return handleContextItem(itemId, trackIds);
public boolean onClick(int itemId, int[] positions, long[] ids) {
return handleContextItem(itemId, ids);
}
};
@@ -121,7 +137,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
if (key != null) {
runOnUiThread(() -> {
TrackListActivity.this.invalidateOptionsMenu();
loaderCallbacks.restart();
LoaderManager.getInstance(TrackListActivity.this).restartLoader(0, null, loaderCallbacks);
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
trackController.update(isRecording, recordingTrackPaused);
});
@@ -133,6 +149,14 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem;
private final OnClickListener stopListener = new OnClickListener() {
@Override
public void onClick(View v) {
updateMenuItems(false, false);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this, true);
}
};
// Callback when the trackRecordingServiceConnection binding changes.
private final Runnable bindChangedCallback = new Runnable() {
@Override
@@ -167,23 +191,42 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
};
private final OnClickListener recordListener = new OnClickListener() {
public void onClick(View v) {
if (!PreferencesUtils.isRecording(recordingTrackId)) {
// Not recording -> Recording
updateMenuItems(false, true);
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
startActivity(newIntent);
} else if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false, true);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(false, true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
// Reset theme after splash
setTheme(R.style.ThemeCustom);
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
gpsStatusValue = GpsStatusValue.GPS_NONE;
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, true, this);
trackController = new TrackController(this, trackRecordingServiceConnection, true, recordListener, stopListener);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
// Show trackController when search dialog is dismissed
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
@@ -191,8 +234,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
searchManager.setOnDismissListener(() -> trackController.show());
}
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> {
listView = findViewById(R.id.track_list);
listView.setEmptyView(findViewById(R.id.track_list_empty_view));
listView.setOnItemClickListener((parent, view, position, trackId) -> {
Intent newIntent;
if (trackId == recordingTrackId.getId()) {
// Is recording -> open record activity.
@@ -209,47 +253,41 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override
public void bindView(View view, Context context, Cursor cursor) {
int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID);
int iconIndex = cursor.getColumnIndexOrThrow(TracksColumns.ICON);
int nameIndex = cursor.getColumnIndexOrThrow(TracksColumns.NAME);
int idIndex = cursor.getColumnIndex(TracksColumns._ID);
int iconIndex = cursor.getColumnIndex(TracksColumns.ICON);
int nameIndex = cursor.getColumnIndex(TracksColumns.NAME);
int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
int categoryIndex = cursor.getColumnIndexOrThrow(TracksColumns.CATEGORY);
int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION);
int markerCountIndex = cursor.getColumnIndexOrThrow(TracksColumns.MARKER_COUNT);
int categoryIndex = cursor.getColumnIndex(TracksColumns.CATEGORY);
int descriptionIndex = cursor.getColumnIndex(TracksColumns.DESCRIPTION);
Track.Id trackId = new Track.Id(cursor.getLong(idIndex));
boolean isRecording = trackId.equals(recordingTrackId);
boolean isRecording = trackId == recordingTrackId;
String icon = cursor.getString(iconIndex);
int iconId = TrackIconUtils.getIconDrawable(icon);
String name = cursor.getString(nameIndex);
String totalTime = StringUtils.formatElapsedTime(cursor.getLong(totalTimeIndex));
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, cursor.getDouble(totalDistanceIndex), metricUnits);
int markerCount = cursor.getInt(markerCountIndex);
int markerCount = contentProviderUtils.getWaypointCount(trackId);
long startTime = cursor.getLong(startTimeIndex);
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
String description = cursor.getString(descriptionIndex);
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording, recordingTrackPaused,
iconId, R.string.image_track, name, totalTime, totalDistance, markerCount,
startTime, true, category, description, false);
startTime, true, category, description, null);
}
};
viewBinding.trackList.setAdapter(resourceCursorAdapter);
ActivityUtils.configureListViewContextualMenu(viewBinding.trackList, contextualActionModeCallback);
listView.setAdapter(resourceCursorAdapter);
loadData(getIntent());
ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
LoaderManager.getInstance(this).initLoader(0, null, loaderCallbacks);
requestGPSPermissions();
}
@Override
protected void setupActionBarBack(Toolbar toolbar) {
setSupportActionBar(toolbar);
toolbar.setNavigationIcon(R.drawable.ic_logo_color_24dp);
}
@Override
protected void onStart() {
super.onStart();
@@ -259,14 +297,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackRecordingServiceConnection.startConnection(this);
}
@Override
protected void onPause() {
super.onPause();
// Update UI
trackController.onPause();
}
@Override
protected void onResume() {
super.onResume();
@@ -277,6 +307,14 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackController.onResume(PreferencesUtils.isRecording(recordingTrackId), recordingTrackPaused);
}
@Override
protected void onPause() {
super.onPause();
// Update UI
trackController.onPause();
}
@Override
protected void onStop() {
super.onStop();
@@ -297,9 +335,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
@Override
protected View getRootView() {
viewBinding = TrackListBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
protected int getLayoutResId() {
return R.layout.track_list;
}
@Override
@@ -327,45 +364,43 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.track_list_start_gps) {
LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
if (locationManager != null && !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
} else {
// Invoke trackRecordingService
if (!isGpsStarted()) {
trackRecordingServiceConnection.startAndBind(this);
bindChangedCallback.run();
Intent intent;
switch (item.getItemId()) {
case R.id.track_list_start_gps:
LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
if (locationManager != null && !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(intent);
} else {
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
trackRecordingService.stopGps();
// Invoke trackRecordingService
if (!isGpsStarted()) {
trackRecordingServiceConnection.startAndBind(this);
bindChangedCallback.run();
} else {
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
trackRecordingService.stopGps();
}
trackRecordingServiceConnection.unbindAndStop(this);
}
trackRecordingServiceConnection.unbindAndStop(this);
// Update menu after starting or stopping gps
this.invalidateOptionsMenu();
}
// Update menu after starting or stopping gps
this.invalidateOptionsMenu();
}
return true;
return true;
case R.id.track_list_aggregated_stats:
intent = IntentUtils.newIntent(this, AggregatedStatisticsActivity.class);
startActivity(intent);
return true;
case R.id.track_list_markers:
intent = IntentUtils.newIntent(this, MarkerListActivity.class);
startActivity(intent);
return true;
case R.id.track_list_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
return true;
}
if (item.getItemId() == R.id.track_list_aggregated_stats) {
startActivity(IntentUtils.newIntent(this, AggregatedStatisticsActivity.class));
return true;
}
if (item.getItemId() == R.id.track_list_markers) {
startActivity(IntentUtils.newIntent(this, MarkerListActivity.class));
return true;
}
if (item.getItemId() == R.id.track_list_settings) {
startActivity(IntentUtils.newIntent(this, SettingsActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
@@ -377,36 +412,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
return super.onKeyUp(keyCode, event);
}
@Override
public void onBackPressed() {
if (loaderCallbacks.getSearchQuery() != null) {
loaderCallbacks.setSearch(null);
return;
}
SearchView searchView = (SearchView) searchMenuItem.getActionView();
if (!searchView.isIconified()) {
searchView.setIconified(true);
return;
}
super.onBackPressed();
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
loadData(intent);
}
private void loadData(Intent intent) {
String searchQuery = null;
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
searchQuery = intent.getStringExtra(SearchManager.QUERY);
}
loaderCallbacks.setSearch(searchQuery);
}
@Override
public boolean onSearchRequested() {
// Hide trackController when search dialog is shown
@@ -420,7 +425,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
@Override
protected void onTrackDeleted() {
protected void onDeleted() {
// Do nothing
}
@@ -476,116 +481,31 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackIds[i] = new Track.Id(longTrackIds[i]);
}
if (itemId == R.id.list_context_menu_show_on_map) {
IntentDashboardUtils.startDashboard(this, false, trackIds);
return true;
Intent intent;
switch (itemId) {
case R.id.list_context_menu_show_on_map:
IntentDashboardUtils.startDashboard(this, false, trackIds);
return true;
case R.id.list_context_menu_share:
intent = IntentUtils.newShareFileIntent(this, trackIds);
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.list_context_menu_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
startActivity(intent);
return true;
case R.id.list_context_menu_delete:
deleteTracks(trackIds);
return true;
case R.id.list_context_menu_select_all:
int size = listView.getCount();
for (int i = 0; i < size; i++) {
listView.setItemChecked(i, true);
}
return false;
}
if (itemId == R.id.list_context_menu_share) {
Intent intent = IntentUtils.newShareFileIntent(this, trackIds);
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
}
if (itemId == R.id.list_context_menu_edit) {
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
startActivity(intent);
return true;
}
if (itemId == R.id.list_context_menu_delete) {
deleteTracks(trackIds);
return true;
}
if (itemId == R.id.list_context_menu_select_all) {
for (int i = 0; i < viewBinding.trackList.getCount(); i++) {
viewBinding.trackList.setItemChecked(i, true);
}
return false;
}
return false;
}
private class TrackLoaderCallBack implements LoaderManager.LoaderCallbacks<Cursor> {
private String searchQuery = null;
public String getSearchQuery() {
return searchQuery;
}
public void setSearch(String searchQuery) {
this.searchQuery = searchQuery;
restart();
if (searchQuery != null) {
setTitle(searchQuery);
} else {
setTitle(R.string.app_name);
}
}
public void restart() {
LoaderManager.getInstance(TrackListActivity.this).restartLoader(0, null, loaderCallbacks);
}
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME,
TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME,
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON, TracksColumns.MARKER_COUNT};
final String sortOrder = TracksColumns.STARTTIME + " DESC";
if (searchQuery == null) {
return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, null, null, sortOrder);
} else {
final String SEARCH_QUERY = TracksColumns.NAME + " LIKE ? OR " +
TracksColumns.DESCRIPTION + " LIKE ? OR " +
TracksColumns.CATEGORY + " LIKE ?";
final String[] selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"};
return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, SEARCH_QUERY, selectionArgs, sortOrder);
}
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
resourceCursorAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
resourceCursorAdapter.swapCursor(null);
}
}
@Override
public void recordStart() {
if (!PreferencesUtils.isRecording(recordingTrackId)) {
// Not recording -> Recording
updateMenuItems(false, true);
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
startActivity(newIntent);
} else if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false, true);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(false, true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
}
@Override
public void recordStop() {
updateMenuItems(false, false);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this, true);
}
}
@@ -21,29 +21,29 @@ import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayoutMediator;
import com.google.android.material.tabs.TabLayout;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.TrackRecordedBinding;
import de.dennisguse.opentracks.fragments.ChartFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
import de.dennisguse.opentracks.fragments.IntervalsFragment;
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity;
import de.dennisguse.opentracks.util.IntentDashboardUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.TrackIconUtils;
import de.dennisguse.opentracks.util.TrackUtils;
/**
* An activity to show the track detail, record a new track or resumes an existing one.
@@ -51,21 +51,21 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class TrackRecordedActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
public class TrackRecordedActivity extends AbstractListActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
public static final String EXTRA_TRACK_ID = "track_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";
// The following are set in onCreate.
private ContentProviderUtils contentProviderUtils;
private TrackDataHub trackDataHub;
private ViewPager pager;
private TrackRecordedBinding viewBinding;
// From intent.
private Track.Id trackId;
@Override
@@ -77,12 +77,46 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
trackDataHub = new TrackDataHub(this);
CustomFragmentPagerAdapter pagerAdapter = new CustomFragmentPagerAdapter(this);
viewBinding.trackDetailActivityViewPager.setAdapter(pagerAdapter);
new TabLayoutMediator(viewBinding.trackDetailActivityTablayout, viewBinding.trackDetailActivityViewPager,
(tab, position) -> tab.setText(pagerAdapter.getPageTitle(position))).attach();
FragmentPagerAdapter adapter = new FragmentPagerAdapter(getSupportFragmentManager(), 1) {
@Override
public int getCount() {
return 3;
}
@NonNull
@Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return new StatisticsRecordedFragment();
case 1:
return ChartFragment.newInstance(false);
case 2:
return ChartFragment.newInstance(true);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
}
}
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return getString(R.string.track_detail_stats_tab);
case 1:
return getString(R.string.settings_chart_by_time);
case 2:
return getString(R.string.settings_chart_by_distance);
}
return "Unknown Tab";
}
};
pager = findViewById(R.id.track_detail_activity_view_pager);
pager.setAdapter(adapter);
TabLayout tabs = findViewById(R.id.track_detail_activity_tablayout);
tabs.setupWithViewPager(pager);
if (savedInstanceState != null) {
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
pager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
}
}
@@ -99,7 +133,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
// Update UI
this.invalidateOptionsMenu();
if (trackId != null) {
if (trackId.isValid()) {
trackDataHub.loadTrack(trackId);
}
}
@@ -113,13 +147,12 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(CURRENT_TAB_TAG_KEY, viewBinding.trackDetailActivityViewPager.getCurrentItem());
outState.putInt(CURRENT_TAB_TAG_KEY, pager.getCurrentItem());
}
@Override
protected View getRootView() {
viewBinding = TrackRecordedBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
protected int getLayoutResId() {
return R.layout.track_detail;
}
@Override
@@ -146,51 +179,43 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.track_detail_share) {
Intent intent = Intent.createChooser(IntentUtils.newShareFileIntent(this, trackId), null);
startActivity(intent);
return true;
Intent intent;
switch (item.getItemId()) {
case R.id.track_detail_share:
intent = IntentUtils.newShareFileIntent(this, trackId);
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.track_detail_menu_show_on_map:
IntentDashboardUtils.startDashboard(this, false, trackId);
return true;
case R.id.track_detail_markers:
intent = IntentUtils.newIntent(this, MarkerListActivity.class)
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_delete:
deleteTracks(trackId);
return true;
case R.id.track_detail_resume_track:
Intent newIntent = IntentUtils.newIntent(TrackRecordedActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
finish();
return true;
case R.id.track_detail_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.startDashboard(this, false, trackId);
return true;
}
if (item.getItemId() == R.id.track_detail_markers) {
Intent intent = IntentUtils.newIntent(this, MarkerListActivity.class)
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
}
if (item.getItemId() == R.id.track_detail_edit) {
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
}
if (item.getItemId() == R.id.track_detail_delete) {
deleteTracks(trackId);
return true;
}
if (item.getItemId() == R.id.track_detail_resume_track) {
Intent newIntent = IntentUtils.newIntent(TrackRecordedActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
finish();
return true;
}
if (item.getItemId() == R.id.track_detail_settings) {
startActivity(IntentUtils.newIntent(this, SettingsActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
@@ -200,7 +225,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
}
@Override
protected void onTrackDeleted() {
protected void onDeleted() {
runOnUiThread(this::finish);
}
@@ -215,15 +240,15 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
private void handleIntent(Intent intent) {
trackId = intent.getParcelableExtra(EXTRA_TRACK_ID);
Marker.Id markerId = intent.getParcelableExtra(EXTRA_MARKER_ID);
if (markerId != null) {
Waypoint.Id waypointId = intent.getParcelableExtra(EXTRA_MARKER_ID);
if (waypointId != null) {
// Use the trackId from the marker
Marker marker = contentProviderUtils.getMarker(markerId);
if (marker == null) {
Waypoint waypoint = contentProviderUtils.getWaypoint(waypointId);
if (waypoint == null) {
finish();
return;
}
trackId = marker.getTrackId();
trackId = waypoint.getTrackId();
}
if (trackId == null) {
@@ -232,47 +257,14 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi
}
}
private class CustomFragmentPagerAdapter extends FragmentStateAdapter {
public void chooseActivityType(String category) {
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), category);
}
public CustomFragmentPagerAdapter(@NonNull FragmentActivity fa) {
super(fa);
}
@NonNull
@Override
public Fragment createFragment(int position) {
switch (position) {
case 0:
return StatisticsRecordedFragment.newInstance(trackId);
case 1:
return IntervalsFragment.newInstance();
case 2:
return ChartFragment.newInstance(false);
case 3:
return ChartFragment.newInstance(true);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
}
}
@Override
public int getItemCount() {
return 4;
}
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return getString(R.string.track_detail_stats_tab);
case 1:
return getString(R.string.track_detail_intervals_tab);
case 2:
return getString(R.string.settings_chart_by_time);
case 3:
return getString(R.string.settings_chart_by_distance);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
}
}
@Override
public void onChooseActivityTypeDone(String iconValue) {
Track track = contentProviderUtils.getTrack(trackId);
String category = getString(TrackIconUtils.getIconActivityType(iconValue));
TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils);
}
}
@@ -9,22 +9,22 @@ import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayoutMediator;
import com.google.android.material.tabs.TabLayout;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.TrackRecordingBinding;
import de.dennisguse.opentracks.fragments.ChartFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.IntervalsFragment;
import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
@@ -43,7 +43,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
*/
//NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key.
//This mode of communication should be removed anyhow.
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, TrackController.Callback {
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface {
public static final String EXTRA_TRACK_ID = "track_id";
@@ -56,10 +56,9 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackDataHub trackDataHub;
private ViewPager pager;
private TrackController trackController;
private TrackRecordingBinding viewBinding;
// Initialized from Intent; if a new track recording is started, a new TrackId will be provided by TrackRecordingService
private Track.Id trackId;
@@ -127,17 +126,46 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
private MenuItem insertMarkerMenuItem;
private MenuItem markerListMenuItem;
private final OnClickListener recordListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
}
};
private final OnClickListener stopListener = new OnClickListener() {
@Override
public void onClick(View v) {
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this, true);
Intent newIntent = IntentUtils.newIntent(TrackRecordingActivity.this, TrackRecordedActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
updateMenuItems(true);
finish();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
contentProviderUtils = new ContentProviderUtils(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackId = null;
if (savedInstanceState != null) {
//Activity was recreated.
trackId = savedInstanceState.getParcelable(EXTRA_TRACK_ID);
} else {
// Resume a track
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
if (trackId != null && contentProviderUtils.getTrack(trackId) == null) {
Log.w(TAG, "TrackId does not exists; cannot continue the recording.");
@@ -146,19 +174,20 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
trackDataHub = new TrackDataHub(this);
CustomFragmentPagerAdapter pagerAdapter = new CustomFragmentPagerAdapter(this);
viewBinding.trackDetailActivityViewPager.setAdapter(pagerAdapter);
new TabLayoutMediator(viewBinding.trackDetailActivityTablayout, viewBinding.trackDetailActivityViewPager,
(tab, position) -> tab.setText(pagerAdapter.getPageTitle(position))).attach();
pager = findViewById(R.id.track_detail_activity_view_pager);
pager.setAdapter(new CustomFragmentPagerAdapter(getSupportFragmentManager(), 1));
TabLayout tabs = findViewById(R.id.track_detail_activity_tablayout);
tabs.setupWithViewPager(pager);
if (savedInstanceState != null) {
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
pager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
}
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, false, this);
trackController = new TrackController(this, trackRecordingServiceConnection, false, recordListener, stopListener);
}
@Override
@@ -236,7 +265,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(CURRENT_TAB_TAG_KEY, viewBinding.trackDetailActivityViewPager.getCurrentItem());
outState.putInt(CURRENT_TAB_TAG_KEY, pager.getCurrentItem());
outState.putParcelable(EXTRA_TRACK_ID, trackId);
}
@@ -255,9 +284,8 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
@Override
protected View getRootView() {
viewBinding = TrackRecordingBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
protected int getLayoutResId() {
return R.layout.track_record;
}
@Override
@@ -278,41 +306,34 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.track_detail_insert_marker) {
Intent intent = IntentUtils
.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
Intent intent;
switch (item.getItemId()) {
case R.id.track_detail_insert_marker:
intent = IntentUtils
.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_menu_show_on_map:
IntentDashboardUtils.startDashboard(this, true, trackId);
return true;
case R.id.track_detail_markers:
intent = IntentUtils.newIntent(this, MarkerListActivity.class)
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.startDashboard(this, true, trackId);
return true;
}
if (item.getItemId() == R.id.track_detail_markers) {
Intent intent = IntentUtils.newIntent(this, MarkerListActivity.class)
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
}
if (item.getItemId() == R.id.track_detail_edit) {
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
}
if (item.getItemId() == R.id.track_detail_settings) {
Intent intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}
/**
@@ -343,49 +364,26 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils);
}
@Override
public void recordStart() {
if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
private class CustomFragmentPagerAdapter extends FragmentPagerAdapter {
public CustomFragmentPagerAdapter(@NonNull FragmentManager fm, int behavior) {
super(fm, behavior);
}
}
@Override
public void recordStop() {
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this, true);
Intent newIntent = IntentUtils.newIntent(TrackRecordingActivity.this, TrackRecordedActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
updateMenuItems(true);
finish();
}
private class CustomFragmentPagerAdapter extends FragmentStateAdapter {
public CustomFragmentPagerAdapter(@NonNull FragmentActivity fa) {
super(fa);
@Override
public int getCount() {
return 3;
}
@NonNull
@Override
public Fragment createFragment(int position) {
public Fragment getItem(int position) {
switch (position) {
case 0:
return StatisticsRecordingFragment.newInstance();
return new StatisticsRecordingFragment();
case 1:
return IntervalsFragment.IntervalsRecordingFragment.newInstance();
case 2:
return ChartFragment.newInstance(false);
case 3:
case 2:
return ChartFragment.newInstance(true);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
@@ -393,23 +391,17 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
@Override
public int getItemCount() {
return 4;
}
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return getString(R.string.track_detail_stats_tab);
case 1:
return getString(R.string.track_detail_intervals_tab);
case 2:
return getString(R.string.settings_chart_by_time);
case 3:
case 2:
return getString(R.string.settings_chart_by_distance);
default:
throw new RuntimeException("There isn't Fragment associated with the position: " + position);
}
}
}
}
}
@@ -46,45 +46,49 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
AggregatedStatistics.AggregatedStatistic aggregatedStatistic = getItem(position);
AggregatedStatistics.AggregatedStatistic aggregatedStats = getItem(position);
ViewHolder viewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.aggregated_stats_list_item, parent, false);
viewHolder = new ViewHolder(convertView);
convertView.setTag(viewHolder);
if (isSpeedSport(position)) {
viewHolder = new ViewSpeedHolder();
} else {
viewHolder = (ViewHolder) convertView.getTag();
viewHolder = new ViewPaceHolder();
}
if (TrackIconUtils.isSpeedIcon(context, aggregatedStatistic.getCategory())) {
viewHolder.setSpeed(aggregatedStatistic);
} else {
viewHolder.setPace(aggregatedStatistic);
}
convertView = LayoutInflater.from(context).inflate(R.layout.aggregated_stats_list_item, parent, false);
viewHolder.setValues(convertView, getIcon(position), getName(position), aggregatedStats);
return convertView;
}
private boolean isSpeedSport(int position) {
String category = aggregatedStatistics.getSportName(position);
return TrackIconUtils.isSpeedIcon(context, category);
}
private int getIcon(int position) {
String iconValue = TrackIconUtils.getIconValue(context, aggregatedStatistics.getSportName(position));
return TrackIconUtils.getIconDrawable(iconValue);
}
private String getName(int position) {
return aggregatedStatistics.getSportName(position);
}
private class ViewHolder {
private final ImageView sportIcon;
private final TextView typeLabel;
private final TextView numTracks;
private final TextView distance;
private final TextView distanceUnit;
private final TextView time;
protected ImageView sportIcon;
protected TextView typeLabel;
protected TextView numTracks;
protected TextView distance;
protected TextView distanceUnit;
protected TextView time;
protected boolean metricsUnits;
protected boolean reportSpeed;
private final TextView avgSpeed;
private final TextView avgSpeedUnit;
private final TextView avgSpeedLabel;
private final TextView maxSpeed;
private final TextView maxSpeedUnit;
private final TextView maxSpeedLabel;
public void setValues(View view, int iconDrawable, String name, AggregatedStatistics.AggregatedStatistic aggregatedStats) {
reportSpeed = PreferencesUtils.isReportSpeed(context, name);
metricsUnits = PreferencesUtils.isMetricUnits(context);
private boolean metricsUnits;
private boolean reportSpeed;
public ViewHolder(View view) {
sportIcon = view.findViewById(R.id.aggregated_stats_sport_icon);
typeLabel = view.findViewById(R.id.aggregated_stats_type_label);
numTracks = view.findViewById(R.id.aggregated_stats_num_tracks);
@@ -92,68 +96,86 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
distanceUnit = view.findViewById(R.id.aggregated_stats_distance_unit);
time = view.findViewById(R.id.aggregated_stats_time);
sportIcon.setImageResource(iconDrawable);
typeLabel.setText(name);
numTracks.setText(StringUtils.valueInParentheses(String.valueOf(aggregatedStats.getCountTracks())));
Pair<String, String> parts = StringUtils.getDistanceParts(context, aggregatedStats.getTrackStatistics().getTotalDistance(), metricsUnits);
distance.setText(parts.first);
distanceUnit.setText(parts.second);
time.setText(StringUtils.formatElapsedTime(aggregatedStats.getTrackStatistics().getMovingTime()));
}
}
private class ViewSpeedHolder extends ViewHolder {
private TextView avgSpeed;
private TextView avgSpeedUnit;
private TextView avgSpeedLabel;
private TextView maxSpeed;
private TextView maxSpeedUnit;
private TextView maxSpeedLabel;
@Override
public void setValues(View view, int iconDrawable, String name, AggregatedStatistics.AggregatedStatistic aggregatedStats) {
super.setValues(view, iconDrawable, name, aggregatedStats);
avgSpeed = view.findViewById(R.id.aggregated_stats_avg_rate);
avgSpeedUnit = view.findViewById(R.id.aggregated_stats_avg_rate_unit);
avgSpeedLabel = view.findViewById(R.id.aggregated_stats_avg_rate_label);
maxSpeed = view.findViewById(R.id.aggregated_stats_max_rate);
maxSpeedUnit = view.findViewById(R.id.aggregated_stats_max_rate_unit);
maxSpeedLabel = view.findViewById(R.id.aggregated_stats_max_rate_label);
}
public void setSpeed(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
setCommonValues(aggregatedStatistic);
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, reportSpeed);
avgSpeed.setText(parts.first);
avgSpeedUnit.setText(parts.second);
avgSpeedLabel.setText(context.getString(R.string.stats_average_moving_speed));
}
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getMaxSpeed(), metricsUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getMaxSpeed(), metricsUnits, reportSpeed);
maxSpeed.setText(parts.first);
maxSpeedUnit.setText(parts.second);
maxSpeedLabel.setText(context.getString(R.string.stats_max_speed));
}
}
}
private class ViewPaceHolder extends ViewHolder {
private TextView avgPace;
private TextView avgPaceUnit;
private TextView avgPaceLabel;
private TextView maxPace;
private TextView maxPaceUnit;
private TextView maxPaceLabel;
@Override
public void setValues(View view, int iconDrawable, String name, AggregatedStatistics.AggregatedStatistic aggregatedStats) {
super.setValues(view, iconDrawable, name, aggregatedStats);
avgPace = view.findViewById(R.id.aggregated_stats_avg_rate);
avgPaceUnit = view.findViewById(R.id.aggregated_stats_avg_rate_unit);
avgPaceLabel = view.findViewById(R.id.aggregated_stats_avg_rate_label);
maxPace = view.findViewById(R.id.aggregated_stats_max_rate);
maxPaceUnit = view.findViewById(R.id.aggregated_stats_max_rate_unit);
maxPaceLabel = view.findViewById(R.id.aggregated_stats_max_rate_label);
public void setPace(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
setCommonValues(aggregatedStatistic);
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, reportSpeed);
avgSpeed.setText(parts.first);
avgSpeedUnit.setText(parts.second);
avgSpeedLabel.setText(context.getString(R.string.stats_average_moving_pace));
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, reportSpeed);
avgPace.setText(parts.first);
avgPaceUnit.setText(parts.second);
avgPaceLabel.setText(R.string.stats_average_moving_pace);
}
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getMaxSpeed(), metricsUnits, reportSpeed);
maxSpeed.setText(parts.first);
maxSpeedUnit.setText(parts.second);
maxSpeedLabel.setText(R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getMaxSpeed(), metricsUnits, reportSpeed);
maxPace.setText(parts.first);
maxPaceUnit.setText(parts.second);
maxPaceLabel.setText(R.string.stats_fastest_pace);
}
}
private void setCommonValues(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
String category = aggregatedStatistic.getCategory();
reportSpeed = PreferencesUtils.isReportSpeed(context, category);
metricsUnits = PreferencesUtils.isMetricUnits(context);
sportIcon.setImageResource(getIcon(aggregatedStatistic));
typeLabel.setText(category);
numTracks.setText(StringUtils.valueInParentheses(String.valueOf(aggregatedStatistic.getCountTracks())));
Pair<String, String> parts = StringUtils.getDistanceParts(context, aggregatedStatistic.getTrackStatistics().getTotalDistance(), metricsUnits);
distance.setText(parts.first);
distanceUnit.setText(parts.second);
time.setText(StringUtils.formatElapsedTime(aggregatedStatistic.getTrackStatistics().getMovingTime()));
}
private int getIcon(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
String iconValue = TrackIconUtils.getIconValue(context, aggregatedStatistic.getCategory());
return TrackIconUtils.getIconDrawable(iconValue);
}
}
}
@@ -1,79 +0,0 @@
package de.dennisguse.opentracks.adapters;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* ExecutorService wrap that avoid the execution of the same object in a ListView.
*
* @param <T> the type of the object that will be used to identified the Runnable.
*/
class ExecutorListViewService<T> {
private List<T> enqueueObjects = new ArrayList<>();
private ExecutorService executorService;
public ExecutorListViewService(int numThreads) {
executorService = Executors.newFixedThreadPool(numThreads);
}
public void shutdown() {
enqueueObjects.clear();
enqueueObjects = null;
executorService.shutdown();
}
/**
* Execute the runnable for the object.
*
* @param object the Object.
* @param runnable the Runnable.
*/
public void execute(T object, Runnable runnable) {
if (!preExecute(object)) {
return;
}
new Thread(() -> {
Future future = executorService.submit(runnable);
try {
future.get();
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
postExecute(object);
}).start();
}
/**
* Before execution it must checks if the object is already enqueued.
*
* @param object the object to check.
* @return true if it can be executed or false otherwise.
*/
private boolean preExecute(T object) {
synchronized (enqueueObjects) {
if (!enqueueObjects.contains(object)) {
enqueueObjects.add(object);
return true;
} else {
return false;
}
}
}
/**
* After execution remove the object from the queue.
*
* @param object the object to be removed from the queue.
*/
private void postExecute(T object) {
synchronized (enqueueObjects) {
enqueueObjects.remove(object);
}
}
}
@@ -1,89 +0,0 @@
package de.dennisguse.opentracks.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.Interval> {
private final StackMode stackMode;
private final boolean metricUnits;
private final String category;
public IntervalStatisticsAdapter(Context context, List<IntervalStatistics.Interval> intervalList, String category, StackMode stackMode) {
super(context, R.layout.interval_stats_list_item, intervalList);
metricUnits = PreferencesUtils.isMetricUnits(context);
this.category = category;
this.stackMode = stackMode;
}
@NonNull
@Override
public View getView(int position, @Nullable View intervalView, @NonNull ViewGroup parent) {
int actualPosition = stackMode == StackMode.STACK_FROM_TOP ? position : getCount() - 1 - position;
IntervalStatistics.Interval interval = getItem(actualPosition);
ViewHolder viewHolder;
if (intervalView == null) {
viewHolder = new ViewHolder();
intervalView = LayoutInflater.from(getContext()).inflate(R.layout.interval_stats_list_item, parent, false);
viewHolder.distance = intervalView.findViewById(R.id.interval_item_distance);
viewHolder.rate = intervalView.findViewById(R.id.interval_item_rate);
viewHolder.gain = intervalView.findViewById(R.id.interval_item_gain);
viewHolder.loss = intervalView.findViewById(R.id.interval_item_loss);
intervalView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) intervalView.getTag();
}
float sumDistance_m;
if (actualPosition + 1 == getCount() && actualPosition > 0) {
sumDistance_m = actualPosition * getItem(actualPosition - 1).getDistance_m() + interval.getDistance_m();
} else {
sumDistance_m = (actualPosition + 1) * interval.getDistance_m();
}
viewHolder.distance.setText(StringUtils.formatDistance(getContext(), sumDistance_m, metricUnits));
if (PreferencesUtils.isReportSpeed(getContext(), category)) {
viewHolder.rate.setText(StringUtils.formatSpeed(getContext(), interval.getSpeed_ms(), metricUnits, true));
} else {
viewHolder.rate.setText(StringUtils.formatSpeed(getContext(), interval.getSpeed_ms(), metricUnits, false));
}
viewHolder.gain.setText(StringUtils.formatDistance(getContext(), interval.getGain_m(), metricUnits));
viewHolder.loss.setText(StringUtils.formatDistance(getContext(), interval.getLoss_m(), metricUnits));
return intervalView;
}
/**
* Defines the two modes of list items stacking: from top or from bottom.
*/
public enum StackMode {
STACK_FROM_BOTTOM,
STACK_FROM_TOP
}
private static class ViewHolder {
private TextView distance;
private TextView rate;
private TextView gain;
private TextView loss;
}
}

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