Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca2bc6426c | |||
| 81097aac40 | |||
| a74b5fc743 | |||
| e996ad0a0c | |||
| 0f726958e0 | |||
| 3a84520819 | |||
| 8328069b9d | |||
| b2df79caf2 | |||
| 6bcb8db40c | |||
| 974baaf611 | |||
| fd77e77bb9 | |||
| 1e8beff015 | |||
| 96bc347374 | |||
| 8f144d4a97 | |||
| 3c3641376d | |||
| 35f4427aec | |||
| 5a0cb10838 | |||
| bf92851c97 | |||
| 55c45b2050 | |||
| d01715444f | |||
| 634a7a4321 | |||
| e5911a79ab | |||
| a78b3eb66d | |||
| 28890abd47 | |||
| 0a466d240b | |||
| 9ef312fc93 | |||
| 56aab12110 | |||
| c394448273 | |||
| 95671f4927 | |||
| 348341204e | |||
| 82f5c127ee | |||
| dd490f528e | |||
| 27731319f0 | |||
| 86bfe6d0ce | |||
| ea8bbb0b36 | |||
| c02fdd3bba | |||
| d16cbe04a5 | |||
| 90a8fc898a | |||
| 8d141b1324 | |||
| 43e0f33776 | |||
| e4270b983a | |||
| 0209a90e01 | |||
| 0f92a37146 | |||
| 9cebb1013c | |||
| 6ac0db7238 | |||
| 3c1ab01df6 | |||
| b6850eb46f | |||
| 08b9cb31fa | |||
| 9541a92657 | |||
| acc2627bde | |||
| 48c3695b5d | |||
| 2b280f050c | |||
| 68f34d9682 | |||
| f56820e132 | |||
| 0f5d72f098 | |||
| accbc5a8ac | |||
| 0bc9f6a042 | |||
| bbb91fedc5 | |||
| 433ab01ece | |||
| 88e1a95da7 | |||
| d4483819eb | |||
| 917b8f4056 | |||
| 70c1379778 | |||
| 5a60f40308 | |||
| df9589697a | |||
| 27443cc716 | |||
| 654be751a7 | |||
| 3a8f3e46c7 | |||
| b270c45691 | |||
| 257d8c1276 | |||
| 8521b98f4b | |||
| eaf655f6e7 | |||
| 24c7f3fd7a | |||
| a4bab656e4 | |||
| 679c1d7e3e | |||
| 0036934fb3 | |||
| c508747203 | |||
| 0f4c0079d7 | |||
| 9e6bb1c511 | |||
| a42bdf32e1 | |||
| c29d69b44d | |||
| 547b1d07bd | |||
| 22a1dfd354 | |||
| be81e758c6 | |||
| b3117e6410 | |||
| 47ab109c2b | |||
| bc04efbaac | |||
| fbb9d21e73 | |||
| f5308e4287 | |||
| 15b5920e3b | |||
| d09aaab398 | |||
| f7008e2362 | |||
| e939778beb | |||
| 8643b50bd1 | |||
| 74e42525d4 | |||
| 833a387574 | |||
| e574c67ad6 | |||
| ce38d5e891 | |||
| 3a44da4a9d | |||
| eba9106db5 | |||
| 4501226562 | |||
| b974193564 | |||
| 0b3e447258 | |||
| 9908cc02a2 | |||
| 4e67011d7c | |||
| 5f93c0bab3 | |||
| caec5e7c45 | |||
| 240cb4b363 | |||
| 91e8eb66b1 | |||
| 27bd69ec62 | |||
| 553fd39e21 | |||
| 733ee1c6e6 | |||
| ef61c9597d | |||
| e874af791e | |||
| a4b2bb1aa0 | |||
| bc9b20ffc8 | |||
| 6f212328a1 | |||
| a68cde7ac9 | |||
| 5cab331058 | |||
| 6d25a10abd | |||
| d2516947f2 | |||
| 4150d060ee | |||
| 1688076457 | |||
| 5c1f669b2b | |||
| cc1e080ce1 | |||
| 0dfd31a308 | |||
| bd13535d3c | |||
| 27386bb4f0 | |||
| c6c07f03bf | |||
| b0ad08f7c5 | |||
| d1957331b8 | |||
| ffedc04777 | |||
| 684762db58 | |||
| 5a44da3fb0 | |||
| 88cf46d999 | |||
| 906b9413d1 | |||
| 060c4f1529 | |||
| 7902aea849 | |||
| ee22fd8ee8 | |||
| a6e275f8aa | |||
| a61468d600 | |||
| 78c26cb669 | |||
| 0d493c410e | |||
| 6e1aba6fa0 | |||
| 5099118dd7 | |||
| 6845ce4984 | |||
| e0d1ad4b52 | |||
| 04b8d0f4a8 | |||
| fe3bf61acb | |||
| 577b9b4635 | |||
| 4b7de188fd | |||
| 3bfb212dc9 | |||
| be7eb41cf3 | |||
| c7db4f528e | |||
| 8a14bc46f6 | |||
| 01ee1a1e49 |
@@ -1,5 +1,26 @@
|
||||
<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,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,194 +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.
|
||||
@@ -2,77 +2,100 @@
|
||||
|
||||
_OpenTracks_ is a sport tracking application that completely respects your privacy.
|
||||
|
||||
[](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>
|
||||
<td align="center">
|
||||
<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>
|
||||
<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 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>
|
||||
<a href="https://www.transifex.com/opentracks/opentracks/">
|
||||
Transifex
|
||||
<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">
|
||||
<a href="https://play.google.com/store/apps/details?id=de.dennisguse.opentracks.playstore">
|
||||
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" height="60" align="middle">
|
||||
</a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 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 required Internet access).
|
||||
_OpenTracks_ does not contain a _map_ (as this would require internet access).
|
||||
|
||||
## Custom Dashboards (incl. Map)
|
||||
As of v3.3.1 OpenTracks enables to use custom dashboards.
|
||||
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) or [MAPS.ME](https://play.google.com/store/apps/details?id=com.mapswithme.maps.pro)).
|
||||
Alternatively, recorded tracks can be shared as KMZ/GPX to installed applications (e.g., [OsmAnd](https://play.google.com/store/apps/details?id=net.osmand)).
|
||||
However, this is rather slow and does not provide updates while recording.
|
||||
|
||||
|
||||
## Screenshots
|
||||
<div>
|
||||
<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">
|
||||
<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">
|
||||
</div>
|
||||
|
||||
## Project history
|
||||
@@ -85,8 +108,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,
|
||||
@@ -96,7 +119,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)
|
||||
|
||||
@@ -34,4 +34,4 @@ Requirement: `gem install fastlane`
|
||||
|
||||
## Translations (localization)
|
||||
|
||||
Translations of the OpenTracks are handled in Transifex: [https://www.transifex.com/opentracks](https://www.transifex.com/opentracks/).
|
||||
Translations of the OpenTracks are handled in Weblate: [https://weblate.bubu1.eu/projects/OpenTracks/](https://weblate.bubu1.eu/projects/OpenTracks/).
|
||||
@@ -1,20 +1,42 @@
|
||||
# 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)
|
||||
|
||||
## CSCP: Cycling Cadence and Speed Service
|
||||
|
||||
Please note that the according to the specification these sensors _may_ provide speed as well as cadence.
|
||||
However, often only one value is provided.
|
||||
|
||||
### Speed
|
||||
|
||||
* Garmin Speed Sensor 2
|
||||
* Wahoo Speed (Model: WFRPMSPD)
|
||||
|
||||
### Cadence
|
||||
### Cadence
|
||||
|
||||
* Garmin Cadence Sensor 2
|
||||
* Wahoo Cadence (Model: WFPODCAD2)
|
||||
This sensor reports cadence data as speed.
|
||||
A workaround is in place.
|
||||
A workaround is in place.
|
||||
|
||||
## Cycling Power Measurement
|
||||
|
||||
We only support _Instantaneous Power_.
|
||||
|
||||
_WARNING_: we only support physically movements with valid GPS coordinates.
|
||||
Using a device indoors (no GPS available) will only show the current power values, but won't store anything.
|
||||
Also the distance is not computed.
|
||||
|
||||
* Assioma Duo Powermeter
|
||||
* Elite drivo II
|
||||
* QUARQ Red DZero Powermeter
|
||||
* Tacx Satori Smart
|
||||
* Wahoo Kickr v4.0
|
||||
@@ -6,7 +6,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.1'
|
||||
classpath 'com.android.tools.build:gradle:4.1.1'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,14 @@ android {
|
||||
|
||||
compileSdkVersion 30
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.dennisguse.opentracks"
|
||||
versionCode 3584
|
||||
versionName "v3.9.5"
|
||||
versionCode 3740
|
||||
versionName "v3.13.2"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
@@ -82,17 +86,17 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
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.1'
|
||||
implementation 'com.android.support.constraint:constraint-layout:2.0.4'
|
||||
|
||||
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.5.10'
|
||||
androidTestImplementation 'org.mockito:mockito-android:3.6.0'
|
||||
}
|
||||
|
||||
@@ -10,15 +10,11 @@ endlegend
|
||||
|
||||
[*] --> TrackListActivity
|
||||
TrackListActivity --> TrackRecordingActivity : Start recording
|
||||
TrackListActivity --> SearchListActivity
|
||||
TrackListActivity --> TrackRecordedActivity : Show track
|
||||
TrackListActivity --> MarkerListActivity : Show all markers
|
||||
TrackListActivity --> AggregatedStatisticsActivity : Aggregated stats
|
||||
TrackListActivity --> TrackRecordedActivity : Show track
|
||||
TrackListActivity --> TrackDeleteActivity
|
||||
|
||||
SearchListActivity --> MarkerDetailActivity : Search result
|
||||
SearchListActivity --> TrackRecordedActivity : Search result
|
||||
|
||||
TrackRecordedActivity --> TrackDeleteActivity
|
||||
TrackRecordedActivity --> TrackEditActivity
|
||||
TrackRecordedActivity --> MarkerListActivity
|
||||
@@ -46,10 +42,14 @@ MarkerDetailActivity --> CoordinateOnMapIntent : URI
|
||||
|
||||
SettingsActivity --> AboutActivity
|
||||
SettingsActivity --> HelpActivity
|
||||
SettingsActivity --> ImportActivity
|
||||
SettingsActivity --> ExportActivity
|
||||
SettingsActivity --> DirectoryChooserActivity
|
||||
SettingsActivity --> DirectoryChooserActivity
|
||||
DirectoryChooserActivity$ImportDirectoryChooserActivity --> ImportActivity
|
||||
DirectoryChooserActivity$ExportDirectoryChooserActivity --> ExportActivity
|
||||
SettingsActivity --> DirectoryChooserActivity.ImportDirectoryChooserActivity
|
||||
SettingsActivity --> DirectoryChooserActivity.ExportDirectoryChooserActivity
|
||||
SettingsActivity --> DirectoryChooserActivity.DefaultTrackExportDirectoryChooserActivity
|
||||
DirectoryChooserActivity.ImportDirectoryChooserActivity --> ImportActivity
|
||||
DirectoryChooserActivity.ExportDirectoryChooserActivity --> ExportActivity
|
||||
|
||||
FileImportIntent --> ImportActivity
|
||||
|
||||
@enduml
|
||||
|
||||
@@ -4,7 +4,8 @@ LocationManager --> TrackRecordingService
|
||||
ElevationSumManager --> TrackRecordingService
|
||||
|
||||
TrackRecordingService --> TrackRecordingServiceConnection
|
||||
TrackRecordingService --> Database
|
||||
TrackRecordingService --> CustomContentProvider
|
||||
CustomContentProvider --> Database
|
||||
|
||||
Database --> CustomContentProvider
|
||||
CustomContentProvider --> TrackDataHub
|
||||
|
||||
@@ -8,7 +8,7 @@ class Track {
|
||||
-icon
|
||||
}
|
||||
Track o-- TrackPoint
|
||||
Track o-- Waypoint
|
||||
Track o-- Marker
|
||||
Track "1" - "1" TripStatistics
|
||||
|
||||
class TripStatistics {
|
||||
@@ -21,7 +21,12 @@ class TripStatistics {
|
||||
- totalElevationGain
|
||||
}
|
||||
|
||||
class TrackPoint extends android.location.Location {
|
||||
note left of TripStatistics
|
||||
The time is based upon device time.
|
||||
end note
|
||||
|
||||
|
||||
class TrackPoint {
|
||||
- id
|
||||
- trackId
|
||||
- longitude
|
||||
@@ -36,7 +41,32 @@ class TrackPoint extends android.location.Location {
|
||||
- sensor_power
|
||||
}
|
||||
|
||||
class Waypoint {
|
||||
note right of TrackPoint
|
||||
TODO
|
||||
TrackPoints are mixing two concepts:
|
||||
1. a GPS measurement provided by the GPS hardware
|
||||
2. and also segment markers.
|
||||
These are marked by PAUSE / RESUME TrackPoints.
|
||||
|
||||
While exporting PAUSE/RESUME are not used to create these segments and these are partly recreated when imported.
|
||||
However, the timestamp of PAUSE/RESUME is not exported and can therefore not be restored.
|
||||
This prevents us from restoring the exact TripStatistics.
|
||||
It is also noteworthy that neither PAUSE/RESUME TrackPoints are used for computing the TrackStatistics.
|
||||
Only the timestamp to _create_ those is used.
|
||||
end note
|
||||
|
||||
note left of TrackPoint
|
||||
TODO
|
||||
The time stored in TrackPoints is either:
|
||||
1. created via GPS: provided by the GPS hardware
|
||||
2. created by the user (pause/resume): device time.
|
||||
|
||||
We know that the GPS time and device time are often not in sync.
|
||||
end note
|
||||
|
||||
TrackPoint "1" - "1" android.location.Location
|
||||
|
||||
class Marker {
|
||||
- id
|
||||
- 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 (Bluetooth LE)
|
||||
* nutze beim Radfahren deine Trittfrequenz- und Geschwindigkeitssensoren sowie Leistungsmesser (Bluetooth LE)
|
||||
* exportiert deine Daten als KML oder GPX
|
||||
* funktioniert ganz ohne Internet
|
||||
@@ -0,0 +1,10 @@
|
||||
v3.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
|
||||
@@ -0,0 +1,10 @@
|
||||
v3.11.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Theming: day/night mode (default: night)
|
||||
- Theming: use Material Design instead of custom element theming
|
||||
- Use targetSDK 30 again (F-Droid signs now with V2)
|
||||
|
||||
Bugfix:
|
||||
- Android 11: export all crashed
|
||||
- TrackDetail not updated in UI after data was changed by user
|
||||
@@ -0,0 +1,4 @@
|
||||
v3.11.1: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Voice announcements reported wrong statistics
|
||||
@@ -0,0 +1,22 @@
|
||||
v3.12.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Bluetooth sensor addresses
|
||||
- Intervals: show elevation gain
|
||||
- Export: default export directory
|
||||
- Export: automatic export after finishing a track
|
||||
- Manual import/export: new user interface
|
||||
- Manual export: option to overwrite on manual export
|
||||
|
||||
Faster:
|
||||
- Track lists loading
|
||||
- Deleting multiple tracks
|
||||
|
||||
Bugfix:
|
||||
- Fixed intent filters for import
|
||||
- KMZ could not be parsed
|
||||
- Export: fix crash on Android 11
|
||||
|
||||
Developer:
|
||||
- Use ViewBindings
|
||||
- Database enforces foreign key and uses cascading deletes
|
||||
@@ -0,0 +1,10 @@
|
||||
v3.13.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Sensor: support for powermeter (BLE)
|
||||
- Search by track and marker (in lists)
|
||||
- Combined search removed
|
||||
|
||||
Bugfix:
|
||||
- Import crashed in UI
|
||||
- Images of markers were not imported
|
||||
@@ -0,0 +1,5 @@
|
||||
v3.13.1: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Crash on record start: minify/shrink removed a drawable
|
||||
- Automatic export failed silently (mainly remote destinations)
|
||||
@@ -0,0 +1,5 @@
|
||||
v3.13.2: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Crash in landscape mode
|
||||
|
||||
@@ -6,5 +6,6 @@ Features:
|
||||
* Supports Bluetooth LE sensors:
|
||||
* heart rate
|
||||
* cycling: speed and cadence
|
||||
* cycling: power meter
|
||||
* Export data: export tracks either as KML or GPX
|
||||
* No Internet access: Internet is not used
|
||||
|
||||
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 144 KiB |
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -82,7 +82,6 @@ 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
|
||||
@@ -130,7 +129,6 @@ 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
|
||||
|
||||
@@ -1,104 +1,103 @@
|
||||
@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
|
||||
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -1,322 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.content;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.rule.provider.ProviderTestRule;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.SearchEngine.ScoredResult;
|
||||
import de.dennisguse.opentracks.content.SearchEngine.SearchQuery;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.CustomContentProvider;
|
||||
import de.dennisguse.opentracks.content.provider.CustomSQLiteOpenHelper;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
/**
|
||||
* Tests for {@link SearchEngine}.
|
||||
* These are not meant to be quality tests, but instead feature-by-feature tests.
|
||||
* In other words, they don't test the mixing of different score boostings, just each boosting separately.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class SearchEngineTest {
|
||||
|
||||
private static final String TAG = SearchEngineTest.class.getSimpleName();
|
||||
|
||||
private static final Location HERE = new Location("gps");
|
||||
private static final long NOW = SearchEngine.OLDEST_ALLOWED_TIMESTAMP + 1000;
|
||||
|
||||
@Rule
|
||||
public ProviderTestRule sqliteContentProviderRule = new ProviderTestRule.Builder(CustomContentProvider.class, ContentProviderUtils.AUTHORITY_PACKAGE).setPrefix(TAG).build();
|
||||
|
||||
private ContentProviderUtils providerUtils;
|
||||
private SearchEngine engine;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
providerUtils = new ContentProviderUtils(sqliteContentProviderRule.getResolver());
|
||||
|
||||
engine = new SearchEngine(providerUtils);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
InstrumentationRegistry.getInstrumentation().getTargetContext().deleteDatabase(TAG + CustomSQLiteOpenHelper.DATABASE_NAME);
|
||||
}
|
||||
|
||||
private Track.Id insertTrack(String title, String description, String category, long hoursAgo) {
|
||||
Track track = new Track();
|
||||
track.setName(title);
|
||||
track.setDescription(description);
|
||||
track.setCategory(category);
|
||||
|
||||
TrackStatistics stats = track.getTrackStatistics();
|
||||
if (hoursAgo > 0) {
|
||||
// Started twice hoursAgo, so the average time is hoursAgo.
|
||||
stats.setStartTime_ms(NOW - hoursAgo * 1000L * 60L * 60L * 2);
|
||||
stats.setStopTime_ms(NOW);
|
||||
}
|
||||
|
||||
Uri uri = providerUtils.insertTrack(track);
|
||||
return new Track.Id(ContentUris.parseId(uri));
|
||||
}
|
||||
|
||||
private Track.Id insertTrack(String title, String description, String category) {
|
||||
return insertTrack(title, description, category, -1);
|
||||
}
|
||||
|
||||
private Track.Id insertTrack(String title, long hoursAgo) {
|
||||
return insertTrack(title, "", "", hoursAgo);
|
||||
}
|
||||
|
||||
private Marker.Id insertWaypoint(String title, String description, String category, double distance, long hoursAgo, Track.Id trackId) {
|
||||
Location location = new Location(HERE);
|
||||
location.setLatitude(location.getLatitude() + distance);
|
||||
location.setLongitude(location.getLongitude() + distance);
|
||||
if (hoursAgo >= 0) {
|
||||
location.setTime(NOW - hoursAgo * 1000L * 60L * 60L);
|
||||
}
|
||||
Marker waypoint = new Marker(location);
|
||||
waypoint.setName(title);
|
||||
waypoint.setDescription(description);
|
||||
waypoint.setCategory(category);
|
||||
waypoint.setTrackId(trackId);
|
||||
|
||||
Uri uri = providerUtils.insertMarker(waypoint);
|
||||
return new Marker.Id(ContentUris.parseId(uri));
|
||||
}
|
||||
|
||||
private Marker.Id insertWaypoint(String title, String description, String category) {
|
||||
return insertWaypoint(title, description, category, 0.0, -1, null);
|
||||
}
|
||||
|
||||
private Marker.Id insertWaypoint(String title, double distance) {
|
||||
return insertWaypoint(title, "", "", distance, -1, null);
|
||||
}
|
||||
|
||||
private Marker.Id insertWaypoint(String title, long hoursAgo) {
|
||||
return insertWaypoint(title, "", "", 0.0, hoursAgo, null);
|
||||
}
|
||||
|
||||
private Marker.Id insertWaypoint(String title, long hoursAgo, Track.Id trackId) {
|
||||
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");
|
||||
Marker.Id descriptionMatchId = insertWaypoint("bb", "aa", "cc");
|
||||
Marker.Id categoryMatchId = insertWaypoint("bb", "cc", "aa");
|
||||
Marker.Id titleMatchId = insertWaypoint("aa", "bb", "cc");
|
||||
Marker.Id titleCategoryMatchId = insertWaypoint("aa", "bb", "ca");
|
||||
Marker.Id titleDescriptionMatchId = insertWaypoint("aa", "ba", "cc");
|
||||
Marker.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
|
||||
Marker.Id descriptionWaypointId = insertWaypoint("bb", "aa", "cc");
|
||||
insertWaypoint("bb", "cc", "dd");
|
||||
Marker.Id titleWaypointId = insertWaypoint("aa", "bb", "cc");
|
||||
insertTrack("bb", "cc", "dd");
|
||||
Track.Id trackId = insertTrack("aa", "bb", "cc");
|
||||
|
||||
SearchQuery query = new SearchQuery("a", null, null, NOW);
|
||||
ArrayList<ScoredResult> results = new ArrayList<>(engine.search(query));
|
||||
|
||||
// Title > Description > Category.
|
||||
assertEquals(results.toString(), 3, results.size());
|
||||
assertTrackResult(trackId, results.get(0));
|
||||
assertWaypointResult(titleWaypointId, results.get(1));
|
||||
assertWaypointResult(descriptionWaypointId, results.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchWaypointDistance() {
|
||||
// All results match text, but they're at difference distances from the user.
|
||||
Marker.Id farFarAwayId = insertWaypoint("aa", 0.3);
|
||||
Marker.Id nearId = insertWaypoint("ab", 0.1);
|
||||
Marker.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.
|
||||
Marker.Id oldestId = insertWaypoint("aa", 2);
|
||||
Marker.Id recentId = insertWaypoint("ab", 0);
|
||||
Marker.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.
|
||||
Marker.Id otherId = insertWaypoint("aa", 1, new Track.Id(456));
|
||||
Marker.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) {
|
||||
assertNotNull("Not a track", result.track);
|
||||
assertNull("Ambiguous result", result.marker);
|
||||
assertEquals(trackId, result.track.getId());
|
||||
}
|
||||
|
||||
private void assertTrackResults(List<ScoredResult> results, Track.Id... trackIds) {
|
||||
long[] longTrackIds = new long[trackIds.length];
|
||||
for (int i = 0; i < trackIds.length; i++) {
|
||||
longTrackIds[i] = trackIds[i].getId();
|
||||
}
|
||||
|
||||
String errMsg = "Expected IDs=" + Arrays.toString(longTrackIds) + "; results=" + results;
|
||||
assertEquals(results.size(), trackIds.length);
|
||||
for (int i = 0; i < results.size(); i++) {
|
||||
ScoredResult result = results.get(i);
|
||||
assertNotNull(errMsg, result.track);
|
||||
assertNull(errMsg, result.marker);
|
||||
assertEquals(errMsg, trackIds[i], result.track.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private void assertWaypointResult(Marker.Id waypointId, ScoredResult result) {
|
||||
assertNotNull("Not a waypoint", result.marker);
|
||||
assertNull("Ambiguous result", result.track);
|
||||
assertEquals(waypointId, result.marker.getId());
|
||||
}
|
||||
|
||||
private void assertWaypointResults(List<ScoredResult> results, Marker.Id... waypointIds) {
|
||||
long[] longWaypointIds = new long[waypointIds.length];
|
||||
for (int i = 0; i < waypointIds.length; i++) {
|
||||
longWaypointIds[i] = waypointIds[i].getId();
|
||||
}
|
||||
|
||||
String errMsg = "Expected IDs=" + Arrays.toString(longWaypointIds) + "; results=" + results;
|
||||
assertEquals(results.size(), waypointIds.length);
|
||||
for (int i = 0; i < results.size(); i++) {
|
||||
ScoredResult result = results.get(i);
|
||||
assertNotNull(errMsg, result.marker);
|
||||
assertNull(errMsg, result.track);
|
||||
assertEquals(errMsg, waypointIds[i], result.marker.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ 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;
|
||||
|
||||
/**
|
||||
* Create a track without any trackPoints.
|
||||
@@ -68,9 +69,9 @@ public class TestDataUtil {
|
||||
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);
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ public class CustomContentProviderUtilsTest {
|
||||
Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
||||
|
||||
Marker waypoint = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
Marker waypoint = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
contentProviderUtils.insertMarker(waypoint);
|
||||
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
@@ -287,8 +287,7 @@ public class CustomContentProviderUtilsTest {
|
||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId2, 10);
|
||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId3, 10);
|
||||
|
||||
Marker waypoint = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId2));
|
||||
waypoint.setTrackId(trackId1);
|
||||
Marker waypoint = new Marker(trackId1, contentProviderUtils.getLastValidTrackPoint(trackId2));
|
||||
contentProviderUtils.insertMarker(waypoint);
|
||||
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
@@ -455,7 +454,7 @@ public class CustomContentProviderUtilsTest {
|
||||
statistics.setTotalTime(1500L);
|
||||
statistics.setMovingTime(700L);
|
||||
statistics.setTotalDistance(750.0);
|
||||
statistics.setTotalElevationGain(50.0);
|
||||
statistics.setTotalElevationGain(50.0f);
|
||||
statistics.setMaxSpeed(60.0);
|
||||
statistics.setMaxElevation(1250.0);
|
||||
statistics.setMinElevation(1200.0);
|
||||
@@ -463,7 +462,7 @@ public class CustomContentProviderUtilsTest {
|
||||
track.first.setTrackStatistics(statistics);
|
||||
contentProviderUtils.insertTrack(track.first);
|
||||
|
||||
Marker waypoint = new Marker(track.second[0]);
|
||||
Marker waypoint = new Marker(trackId, track.second[0]);
|
||||
waypoint.setDescription(TEST_DESC);
|
||||
contentProviderUtils.insertMarker(waypoint);
|
||||
|
||||
@@ -522,9 +521,8 @@ public class CustomContentProviderUtilsTest {
|
||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
||||
|
||||
// Insert at first.
|
||||
Marker waypoint1 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
Marker waypoint1 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
waypoint1.setDescription(TEST_DESC);
|
||||
waypoint1.setTrackId(trackId);
|
||||
contentProviderUtils.insertMarker(waypoint1);
|
||||
|
||||
// Check insert was done.
|
||||
@@ -600,14 +598,12 @@ public class CustomContentProviderUtilsTest {
|
||||
// TestDataUtil.insertTrackWithLocations(contentProviderUtils, track);
|
||||
|
||||
// Insert at first.
|
||||
Marker waypoint1 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
Marker waypoint1 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
waypoint1.setDescription(MOCK_DESC);
|
||||
waypoint1.setTrackId(trackId);
|
||||
Marker.Id waypoint1Id = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint1)));
|
||||
|
||||
Marker waypoint2 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
Marker waypoint2 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
waypoint2.setDescription(MOCK_DESC);
|
||||
waypoint2.setTrackId(trackId);
|
||||
Marker.Id waypoint2Id = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint2)));
|
||||
|
||||
// Delete
|
||||
@@ -626,14 +622,10 @@ public class CustomContentProviderUtilsTest {
|
||||
Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
||||
|
||||
Marker waypoint1 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
waypoint1.setTrackId(trackId);
|
||||
Marker waypoint2 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
waypoint2.setTrackId(trackId);
|
||||
Marker waypoint3 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
waypoint3.setTrackId(trackId);
|
||||
Marker waypoint4 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
waypoint4.setTrackId(trackId);
|
||||
Marker waypoint1 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
Marker waypoint2 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
Marker waypoint3 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
Marker waypoint4 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
contentProviderUtils.insertMarker(waypoint1);
|
||||
contentProviderUtils.insertMarker(waypoint2);
|
||||
contentProviderUtils.insertMarker(waypoint3);
|
||||
@@ -651,9 +643,8 @@ public class CustomContentProviderUtilsTest {
|
||||
Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
||||
|
||||
Marker waypoint = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
Marker waypoint = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
waypoint.setDescription(TEST_DESC);
|
||||
waypoint.setTrackId(trackId);
|
||||
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
|
||||
|
||||
assertEquals(TEST_DESC, contentProviderUtils.getMarker(waypointId).getDescription());
|
||||
@@ -668,9 +659,8 @@ public class CustomContentProviderUtilsTest {
|
||||
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
|
||||
|
||||
// Insert at first.
|
||||
Marker waypoint = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
Marker waypoint = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
|
||||
waypoint.setDescription(TEST_DESC);
|
||||
waypoint.setTrackId(trackId);
|
||||
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
|
||||
|
||||
// Update
|
||||
@@ -695,7 +685,6 @@ public class CustomContentProviderUtilsTest {
|
||||
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
|
||||
Marker waypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
|
||||
waypoint.setDescription(TEST_DESC);
|
||||
waypoint.setTrackId(trackId);
|
||||
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
|
||||
|
||||
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId.getId());
|
||||
@@ -731,7 +720,6 @@ public class CustomContentProviderUtilsTest {
|
||||
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
|
||||
Marker waypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
|
||||
waypoint.setDescription(TEST_DESC);
|
||||
waypoint.setTrackId(trackId);
|
||||
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
|
||||
|
||||
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId.getId());
|
||||
@@ -766,10 +754,8 @@ public class CustomContentProviderUtilsTest {
|
||||
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
|
||||
Marker waypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
|
||||
waypoint.setDescription(TEST_DESC);
|
||||
waypoint.setTrackId(trackId);
|
||||
Marker otherWaypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
|
||||
otherWaypoint.setDescription(TEST_DESC);
|
||||
otherWaypoint.setTrackId(trackId);
|
||||
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
|
||||
contentProviderUtils.insertMarker(otherWaypoint);
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
@@ -38,39 +37,21 @@ public class CustomSQLiteOpenHelperTest {
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
/**
|
||||
* Get the SQL create statements for all tables (ordered by name).
|
||||
* Get the SQL create statements for all SQLite elements of type (ordered by name).
|
||||
*
|
||||
* @return Map(TableName, SQL)
|
||||
* @param type index, table
|
||||
* @return Map(name, SQL)
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static Map<String, String> 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 + "', '" + MarkerColumns.TABLE_NAME + "')", null, null, null, "name")) {
|
||||
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")) {
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
tableSQL.put(cursor.getString(0), cursor.getString(1));
|
||||
sqlMap.put(cursor.getString(0), cursor.getString(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
return sqlMap;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,7 +90,7 @@ public class CustomSQLiteOpenHelperTest {
|
||||
assertTrue(hasSqlCreate(db, MarkerColumns.CREATE_TABLE));
|
||||
assertTrue(hasSqlCreate(db, MarkerColumns.CREATE_TABLE_INDEX));
|
||||
} catch (Exception e) {
|
||||
fail();
|
||||
fail("Database could not be created: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,8 +102,8 @@ public class CustomSQLiteOpenHelperTest {
|
||||
Map<String, String> tableByUpgrade;
|
||||
Map<String, String> indicesByUpgrade;
|
||||
try (SQLiteDatabase dbUpgraded = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase()) {
|
||||
tableByUpgrade = getTableSQL(dbUpgraded);
|
||||
indicesByUpgrade = getIndexSQL(dbUpgraded);
|
||||
tableByUpgrade = getSQL(dbUpgraded, "table");
|
||||
indicesByUpgrade = getSQL(dbUpgraded, "index");
|
||||
}
|
||||
context.deleteDatabase(DATABASE_NAME);
|
||||
|
||||
@@ -130,12 +111,14 @@ public class CustomSQLiteOpenHelperTest {
|
||||
Map<String, String> tablesByCreate;
|
||||
Map<String, String> indicesByCreate;
|
||||
try (SQLiteDatabase dbCreated = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase()) {
|
||||
tablesByCreate = getTableSQL(dbCreated);
|
||||
indicesByCreate = getIndexSQL(dbCreated);
|
||||
tablesByCreate = getSQL(dbCreated, "table");
|
||||
indicesByCreate = getSQL(dbCreated, "index");
|
||||
}
|
||||
|
||||
|
||||
// then - verify table structure
|
||||
assertEquals(3, tableByUpgrade.size());
|
||||
int tableCount = 3 + 2; //Three with data tables + two SQLite
|
||||
assertEquals(tableCount, tableByUpgrade.size());
|
||||
assertEquals(tableByUpgrade.size(), tablesByCreate.size());
|
||||
|
||||
assertEquals(tablesByCreate.get(TracksColumns.TABLE_NAME), tableByUpgrade.get(TracksColumns.TABLE_NAME));
|
||||
@@ -158,8 +141,8 @@ public class CustomSQLiteOpenHelperTest {
|
||||
Map<String, String> tablesByDowngrade;
|
||||
Map<String, String> indicesByDowngrade;
|
||||
try (SQLiteDatabase db = new CustomSQLiteOpenHelper(context, DATABASE_NAME, 23).getReadableDatabase()) {
|
||||
tablesByDowngrade = getTableSQL(db);
|
||||
indicesByDowngrade = getIndexSQL(db);
|
||||
tablesByDowngrade = getSQL(db, "table");
|
||||
indicesByDowngrade = getSQL(db, "index");
|
||||
}
|
||||
|
||||
// then - verify table structure
|
||||
|
||||
@@ -9,6 +9,7 @@ import de.dennisguse.opentracks.util.UintUtils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SensorDataCyclingTest {
|
||||
@@ -117,4 +118,32 @@ public class SensorDataCyclingTest {
|
||||
// then
|
||||
assertEquals(2, current.getSpeed_mps(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equals_speed_with_no_data() {
|
||||
// given
|
||||
SensorDataCycling.Speed previous = new SensorDataCycling.Speed("sensorAddress");
|
||||
SensorDataCycling.Speed current = new SensorDataCycling.Speed("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
previous.toString();
|
||||
|
||||
// then
|
||||
assertNotEquals(previous, current);
|
||||
assertNotEquals(previous, previous);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equals_cadence_with_no_data() {
|
||||
// given
|
||||
SensorDataCycling.Cadence previous = new SensorDataCycling.Cadence("sensorAddress");
|
||||
SensorDataCycling.Cadence current = new SensorDataCycling.Cadence("sensorAddress", "sensorName", 0, 2048);
|
||||
|
||||
// when
|
||||
previous.toString();
|
||||
|
||||
// then
|
||||
assertNotEquals(previous, current);
|
||||
assertNotEquals(previous, previous);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import androidx.test.filters.LargeTest;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
@@ -72,13 +73,12 @@ public class ExportImportTest {
|
||||
trackPoints.addAll(Arrays.asList(track.second));
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
Marker marker = new Marker(track.second[i].getLocation());
|
||||
Marker marker = new Marker(trackId, track.second[i].getLocation());
|
||||
marker.setName("the marker " + i);
|
||||
marker.setDescription("the marker description " + i);
|
||||
marker.setCategory("the marker category" + i);
|
||||
marker.setIcon("the waypoing icon" + i);
|
||||
marker.setIcon("the marker icon" + i);
|
||||
marker.setPhotoUrl("the photo url" + i);
|
||||
marker.setTrackId(trackId);
|
||||
contentProviderUtils.insertMarker(marker);
|
||||
|
||||
markers.add(marker);
|
||||
@@ -90,7 +90,9 @@ public class ExportImportTest {
|
||||
@After
|
||||
public void tearDown() {
|
||||
contentProviderUtils.deleteTrack(context, trackId);
|
||||
contentProviderUtils.deleteTrack(context, importTrackId);
|
||||
if (importTrackId != null) {
|
||||
contentProviderUtils.deleteTrack(context, importTrackId);
|
||||
}
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@@ -133,7 +135,7 @@ public class ExportImportTest {
|
||||
assertMarkers();
|
||||
|
||||
// 3. trackpoints
|
||||
assertTrackpoints(false, false, false);
|
||||
assertTrackpoints(false, false, false, false);
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@@ -168,11 +170,11 @@ public class ExportImportTest {
|
||||
assertMarkers();
|
||||
|
||||
// 3. trackpoints
|
||||
assertTrackpoints(true, true, true);
|
||||
assertTrackpoints(true, true, true, true);
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
@Test(expected = ImportAlreadyExistsException.class)
|
||||
public void kml_with_trackdetail_and_sensordata_duplicate_trackUUID() {
|
||||
// given
|
||||
PreferencesUtils.setBoolean(context, R.string.import_prevent_reimport_key, true);
|
||||
@@ -195,6 +197,7 @@ public class ExportImportTest {
|
||||
assertNull(importedTrack);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kmz_only_track() {
|
||||
@@ -202,6 +205,7 @@ public class ExportImportTest {
|
||||
Log.e(TAG, "Test not implemented.");
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kmz_with_trackdetail() {
|
||||
@@ -209,6 +213,7 @@ public class ExportImportTest {
|
||||
Log.e(TAG, "Test not implemented.");
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kmz_with_trackdetail_and_sensordata() {
|
||||
@@ -216,6 +221,7 @@ public class ExportImportTest {
|
||||
Log.e(TAG, "Test not implemented.");
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@LargeTest
|
||||
@Test
|
||||
public void kmz_with_trackdetail_and_sensordata_and_pictures() {
|
||||
@@ -257,11 +263,11 @@ public class ExportImportTest {
|
||||
assertMarkers();
|
||||
|
||||
// 3. trackpoints
|
||||
assertTrackpoints(false, true, true);
|
||||
assertTrackpoints(false, true, true, false);
|
||||
}
|
||||
|
||||
@LargeTest
|
||||
@Test
|
||||
@Test(expected = ImportAlreadyExistsException.class)
|
||||
public void gpx_duplicate_trackUUID() {
|
||||
// given
|
||||
PreferencesUtils.setBoolean(context, R.string.import_prevent_reimport_key, true);
|
||||
@@ -304,7 +310,7 @@ public class ExportImportTest {
|
||||
}
|
||||
}
|
||||
|
||||
private void assertTrackpoints(boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence) {
|
||||
private void assertTrackpoints(boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyElevationGain) {
|
||||
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
|
||||
assertEquals(trackPoints.size(), importedTrackPoints.size());
|
||||
|
||||
@@ -330,6 +336,9 @@ public class ExportImportTest {
|
||||
if (verifyPower) {
|
||||
assertEquals(trackPoint.getPower(), importedTrackPoint.getPower(), 0.01);
|
||||
}
|
||||
if (verifyElevationGain) {
|
||||
assertEquals(trackPoint.getElevationGain(), importedTrackPoint.getElevationGain(), 0.01);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@ 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;
|
||||
@@ -34,6 +33,7 @@ import androidx.test.rule.ServiceTestRule;
|
||||
import org.junit.After;
|
||||
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;
|
||||
@@ -235,7 +235,7 @@ public class TrackRecordingServiceTest {
|
||||
assertEquals(TrackPointsColumns.RESUME_LATITUDE, trackPoints.get(2).getLatitude(), 0.01);
|
||||
}
|
||||
|
||||
@FlakyTest(detail = "Sometimes fails on CI.")
|
||||
@Ignore("Sometimes fails on CI.")
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testRecording_orphanedRecordingTrack() throws Exception {
|
||||
@@ -318,7 +318,7 @@ public class TrackRecordingServiceTest {
|
||||
// then
|
||||
assertNotEquals(-1L, waypointId);
|
||||
Marker wpt = contentProviderUtils.getMarker(waypointId);
|
||||
assertEquals(context.getString(R.string.marker_waypoint_icon_url), wpt.getIcon());
|
||||
assertEquals(context.getString(R.string.marker_icon_url), wpt.getIcon());
|
||||
assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
|
||||
assertEquals(trackId, wpt.getTrackId());
|
||||
assertEquals(0.0, wpt.getLength(), 0.01);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
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 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());
|
||||
}
|
||||
}
|
||||
@@ -51,8 +51,8 @@ public class TrackStatisticsTest {
|
||||
statistics2.setMovingTime(600L); // Result: 700+600
|
||||
statistics.setTotalDistance(750.0);
|
||||
statistics2.setTotalDistance(350.0); // Result: 750+350
|
||||
statistics.setTotalElevationGain(50.0);
|
||||
statistics2.setTotalElevationGain(850.0); // Result: 850+50
|
||||
statistics.setTotalElevationGain(50.0f);
|
||||
statistics2.setTotalElevationGain(850.0f); // Result: 850+50
|
||||
statistics.setMaxSpeed(60.0); // Resulting max speed
|
||||
statistics2.setMaxSpeed(30.0);
|
||||
statistics.setMaxElevation(1250.0);
|
||||
|
||||
@@ -59,7 +59,7 @@ public class AnnouncementUtilsTest {
|
||||
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 = " With a current kilometer speed of " + context.getResources().getQuantityString(speedId, getQuantityCount(kmPerHour), kmPerHour);
|
||||
String rateMsg = " Lap speed of " + context.getResources().getQuantityString(speedId, getQuantityCount(kmPerHour), kmPerHour);
|
||||
String msg = firstPartMsg + rateMsg;
|
||||
|
||||
// when
|
||||
|
||||
@@ -75,4 +75,16 @@ public class BluetoothUtilsTest {
|
||||
assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
|
||||
assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCyclingPower_power() {
|
||||
BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(BluetoothUtils.CYCLING_POWER_MEASUREMENT_CHAR_UUID, 0, 0);
|
||||
characteristic.setValue(new byte[]{0, 0, 40, 0});
|
||||
|
||||
// when
|
||||
int power_w = BluetoothUtils.parseCyclingPower(characteristic);
|
||||
|
||||
// then
|
||||
assertEquals(40, power_w);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ public class AggregatedStatisticsTest {
|
||||
statistics.setTotalTime(totalTime);
|
||||
statistics.setMovingTime(totalTime);
|
||||
statistics.setTotalDistance(totalDistance);
|
||||
statistics.setTotalElevationGain(50.0);
|
||||
statistics.setTotalElevationGain(50.0f);
|
||||
statistics.setMaxSpeed(50.0); // Resulting max speed
|
||||
statistics.setMaxElevation(1250.0);
|
||||
statistics.setMinElevation(1200.0); // Resulting min elevation
|
||||
|
||||
@@ -80,7 +80,7 @@ public class IntervalStatisticsTest {
|
||||
// given
|
||||
List<TrackPoint> trackPoints = buildTrackPoints(200);
|
||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||
float distanceInterval = 3000;
|
||||
float distanceInterval = 3000f;
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics();
|
||||
|
||||
// when and then
|
||||
@@ -97,7 +97,7 @@ public class IntervalStatisticsTest {
|
||||
// given
|
||||
List<TrackPoint> trackPoints = buildTrackPoints(1000);
|
||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||
float distanceInterval = 3000;
|
||||
float distanceInterval = 3000f;
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics();
|
||||
|
||||
// when and then
|
||||
@@ -114,7 +114,7 @@ public class IntervalStatisticsTest {
|
||||
// given
|
||||
List<TrackPoint> trackPoints = buildTrackPoints(10000);
|
||||
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
|
||||
float distanceInterval = 1000;
|
||||
float distanceInterval = 1000f;
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics();
|
||||
|
||||
// when and then
|
||||
@@ -126,15 +126,18 @@ public class IntervalStatisticsTest {
|
||||
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();
|
||||
|
||||
@@ -14,7 +14,6 @@ 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">
|
||||
|
||||
@@ -69,38 +68,170 @@ limitations under the License.
|
||||
|
||||
<activity android:name=".io.file.importer.DirectoryChooserActivity$ImportDirectoryChooserActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".io.file.importer.ImportActivity"
|
||||
android:theme="@style/ThemeCustomTransparentActivity">
|
||||
<activity android:name=".io.file.importer.ImportActivity">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="application/gpx+xml"/>
|
||||
</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" />
|
||||
<data android:mimeType="application/vnd.google-earth.kmz" />
|
||||
<data
|
||||
android:scheme="content"
|
||||
android:host="*"
|
||||
android:pathPattern="*kmz" />
|
||||
<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" />
|
||||
<data android:mimeType="application/vnd.google-earth.kml+xml" />
|
||||
<data
|
||||
android:scheme="content"
|
||||
android:host="*"
|
||||
android:pathPattern="*kml" />
|
||||
<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" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="application/vnd.google-earth.kml+xml"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter
|
||||
android:label="@string/app_name"
|
||||
android:priority="50">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/gpx+xml" />
|
||||
<data
|
||||
android:scheme="content"
|
||||
android:host="*"
|
||||
android:pathPattern="*gpx" />
|
||||
<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" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
@@ -115,16 +246,24 @@ limitations under the License.
|
||||
|
||||
<activity
|
||||
android:name=".MarkerListActivity"
|
||||
android:label="@string/menu_markers">
|
||||
android:label="@string/menu_markers"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.default_searchable"
|
||||
android:value=".SearchListActivity" />
|
||||
android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".SearchListActivity"
|
||||
android:name=".TrackListActivity"
|
||||
android:theme="@style/SplashTheme"
|
||||
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
|
||||
@@ -138,23 +277,12 @@ 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"
|
||||
android:theme="@style/ThemeCustomTransparentActivity" />
|
||||
<activity android:name=".io.file.exporter.ExportActivity" />
|
||||
|
||||
<activity android:name=".settings.SettingsActivity" />
|
||||
|
||||
@@ -194,5 +322,15 @@ 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,38 +2,35 @@ package de.dennisguse.opentracks;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.widget.TextView;
|
||||
import android.view.View;
|
||||
|
||||
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));
|
||||
|
||||
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)));
|
||||
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)));
|
||||
|
||||
ViewUtils.makeClickableLinks(findViewById(android.R.id.content));
|
||||
}
|
||||
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.about;
|
||||
protected View getRootView() {
|
||||
viewBinding = AboutBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
@@ -35,14 +36,11 @@ public abstract class AbstractActivity extends AppCompatActivity {
|
||||
// Set volume control stream for text to speech
|
||||
setVolumeControlStream(TTSUtils.getTTSStream());
|
||||
|
||||
setContentView(getLayoutResId());
|
||||
setContentView(getRootView());
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the layout resource id.
|
||||
*/
|
||||
protected abstract int getLayoutResId();
|
||||
protected abstract View getRootView();
|
||||
}
|
||||
|
||||
@@ -30,8 +30,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
/**
|
||||
* An abstract class for the following common tasks across
|
||||
* {@link TrackListActivity}, {@link TrackRecordedActivity}, and
|
||||
* {@link SearchListActivity}:
|
||||
* {@link TrackListActivity}, {@link TrackRecordedActivity}, and {@link TrackRecordedActivity}:
|
||||
* <p>
|
||||
* - share track <br>
|
||||
* - delete tracks <br>
|
||||
@@ -48,7 +47,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == DELETE_REQUEST_CODE) {
|
||||
onDeleted();
|
||||
onTrackDeleted();
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
@@ -93,5 +92,5 @@ public abstract class AbstractListActivity extends AbstractActivity implements C
|
||||
/**
|
||||
* Called after {@link TrackDeleteActivity} returns its result.
|
||||
*/
|
||||
abstract protected void onDeleted();
|
||||
abstract protected void onTrackDeleted();
|
||||
}
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.ListView;
|
||||
import android.view.View;
|
||||
|
||||
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 ListView listView;
|
||||
private AggregatedStatsBinding viewBinding;
|
||||
|
||||
private AggregatedStatisticsAdapter adapter;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
listView = findViewById(R.id.aggregated_stats_list);
|
||||
listView.setEmptyView(findViewById(R.id.aggregated_stats_empty_view));
|
||||
viewBinding.aggregatedStatsList.setEmptyView(viewBinding.aggregatedStatsEmptyView);
|
||||
|
||||
final AggregatedStatisticsModel viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class);
|
||||
viewModel.getAggregatedStats().observe(this, aggregatedStatistics -> {
|
||||
if (aggregatedStatistics != null) {
|
||||
adapter = new AggregatedStatisticsAdapter(getApplicationContext(), aggregatedStatistics);
|
||||
listView.setAdapter(adapter);
|
||||
adapter = new AggregatedStatisticsAdapter(this, aggregatedStatistics);
|
||||
viewBinding.aggregatedStatsList.setAdapter(adapter);
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.aggregated_stats;
|
||||
protected View getRootView() {
|
||||
viewBinding = AggregatedStatsBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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 {
|
||||
@@ -15,7 +17,7 @@ public class HelpActivity extends AbstractActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.help;
|
||||
protected View getRootView() {
|
||||
return HelpBinding.inflate(getLayoutInflater()).getRoot();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ 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;
|
||||
@@ -32,6 +33,7 @@ import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
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;
|
||||
|
||||
@@ -48,33 +50,35 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
|
||||
|
||||
private static final String TAG = MarkerDetailActivity.class.getSimpleName();
|
||||
|
||||
private List<Marker.Id> waypointIds;
|
||||
private MarkerDetailActivityBinding viewBinding;
|
||||
|
||||
private List<Marker.Id> markerIds;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
Marker.Id waypointId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
|
||||
if (waypointId == null) {
|
||||
Marker.Id markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
|
||||
if (markerId == null) {
|
||||
Log.d(TAG, "invalid marker id");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
|
||||
Marker waypoint = contentProviderUtils.getMarker(waypointId);
|
||||
Marker marker = contentProviderUtils.getMarker(markerId);
|
||||
|
||||
waypointIds = new ArrayList<>();
|
||||
markerIds = new ArrayList<>();
|
||||
int markerIndex = -1;
|
||||
|
||||
//TODO Load only waypointIds, not the whole waypoint
|
||||
try (Cursor cursor = contentProviderUtils.getMarkerCursor(waypoint.getTrackId(), null, -1)) {
|
||||
//TODO Load only markerIds, not the whole marker
|
||||
try (Cursor cursor = contentProviderUtils.getMarkerCursor(marker.getTrackId(), null, -1)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
for (int i = 0; i < cursor.getCount(); i++) {
|
||||
Marker currentMarker = contentProviderUtils.createMarker(cursor);
|
||||
waypointIds.add(currentMarker.getId());
|
||||
if (waypointId.equals(currentMarker.getId())) {
|
||||
markerIndex = waypointIds.size() - 1;
|
||||
markerIds.add(currentMarker.getId());
|
||||
if (markerId.equals(currentMarker.getId())) {
|
||||
markerIndex = markerIds.size() - 1;
|
||||
}
|
||||
|
||||
cursor.moveToNext();
|
||||
@@ -82,10 +86,9 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
|
||||
}
|
||||
}
|
||||
|
||||
final ViewPager viewPager = findViewById(R.id.maker_detail_activity_view_pager);
|
||||
final MarkerDetailPagerAdapter markerAdapter = new MarkerDetailPagerAdapter(getSupportFragmentManager());
|
||||
viewPager.setAdapter(markerAdapter);
|
||||
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||
viewBinding.makerDetailActivityViewPager.setAdapter(markerAdapter);
|
||||
viewBinding.makerDetailActivityViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
@@ -100,16 +103,17 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
}
|
||||
});
|
||||
viewPager.setCurrentItem(markerIndex == -1 ? 0 : markerIndex);
|
||||
viewBinding.makerDetailActivityViewPager.setCurrentItem(markerIndex == -1 ? 0 : markerIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.marker_detail_activity;
|
||||
protected View getRootView() {
|
||||
viewBinding = MarkerDetailActivityBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteMarkerDone() {
|
||||
public void onMarkerDeleted() {
|
||||
runOnUiThread(this::finish);
|
||||
}
|
||||
|
||||
@@ -127,7 +131,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
|
||||
@Override
|
||||
@NonNull
|
||||
public Fragment getItem(int position) {
|
||||
return MarkerDetailFragment.newInstance(waypointIds.get(position));
|
||||
return MarkerDetailFragment.newInstance(markerIds.get(position));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -138,7 +142,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return waypointIds.size();
|
||||
return markerIds.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,10 +30,6 @@ 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;
|
||||
@@ -41,10 +37,13 @@ import androidx.annotation.NonNull;
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.databinding.MarkerEditBinding;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
@@ -66,98 +65,125 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
|
||||
private static final String TAG = MarkerEditActivity.class.getSimpleName();
|
||||
private Track.Id trackId;
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
|
||||
private Marker waypoint;
|
||||
private final TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
|
||||
private Marker marker;
|
||||
|
||||
private MenuItem insertPhotoMenuItem;
|
||||
private MenuItem insertGalleryImgMenuItem;
|
||||
|
||||
private Uri photoUri;
|
||||
private Uri photoUriOriginal;
|
||||
private List<Uri> photoUriDeleteList = new ArrayList<>();
|
||||
private boolean hasCamera;
|
||||
|
||||
private boolean isNewMarker;
|
||||
|
||||
// UI elements
|
||||
private EditText waypointName;
|
||||
private AutoCompleteTextView waypointMarkerType;
|
||||
private EditText waypointDescription;
|
||||
private ImageView waypointPhoto;
|
||||
private ImageView waypointDeletePhotoBtn;
|
||||
private MarkerEditBinding viewBinding;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
trackId = getIntent().getParcelableExtra(EXTRA_TRACK_ID);
|
||||
Marker.Id waypointId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
|
||||
Marker.Id markerId = getIntent().getParcelableExtra(EXTRA_MARKER_ID);
|
||||
|
||||
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|
||||
|
||||
// Setup UI elements
|
||||
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);
|
||||
|
||||
waypointDeletePhotoBtn = findViewById(R.id.marker_edit_waypoint_photo_delete);
|
||||
waypointDeletePhotoBtn.setOnClickListener(v -> {
|
||||
if (waypoint != null && waypoint.hasPhoto()) {
|
||||
waypoint.setPhotoUrl(null);
|
||||
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 -> {
|
||||
if (marker != null && marker.hasPhoto()) {
|
||||
marker.setPhotoUrl(null);
|
||||
}
|
||||
waypointPhoto.setImageBitmap(null);
|
||||
viewBinding.markerEditPhoto.setImageBitmap(null);
|
||||
photoUriDeleteList.add(photoUri);
|
||||
photoUriOriginal = photoUriOriginal == null ? photoUri : photoUriOriginal;
|
||||
photoUri = null;
|
||||
hideAndShowOptions();
|
||||
});
|
||||
|
||||
Button cancel = findViewById(R.id.marker_edit_cancel);
|
||||
cancel.setOnClickListener(v -> finish());
|
||||
viewBinding.markerEditCancel.setOnClickListener(v -> {
|
||||
Track.Id trackId = getTrackId();
|
||||
if (isNewMarker && trackId != null) {
|
||||
// new marker and user cancel -> delete all photos from track directory
|
||||
for (Uri photoUriDelete : photoUriDeleteList) {
|
||||
File photoFile = FileUtils.getPhotoFileIfExists(this, trackId, photoUriDelete);
|
||||
FileUtils.deleteDirectoryRecurse(photoFile);
|
||||
}
|
||||
if (photoUri != null) {
|
||||
File photoFile = FileUtils.getPhotoFileIfExists(this, trackId, photoUri);
|
||||
FileUtils.deleteDirectoryRecurse(photoFile);
|
||||
}
|
||||
} else if (!isNewMarker && trackId != null) {
|
||||
// no new marker, user cancel and photo was changed -> delete all photos but original one
|
||||
for (Uri photoUri : photoUriDeleteList) {
|
||||
if (photoUriOriginal != photoUri) {
|
||||
File photoFile = FileUtils.getPhotoFileIfExists(this, trackId, photoUri);
|
||||
FileUtils.deleteDirectoryRecurse(photoFile);
|
||||
}
|
||||
}
|
||||
if (photoUri != null && photoUri != photoUriOriginal) {
|
||||
File photoFile = FileUtils.getPhotoFileIfExists(this, trackId, photoUri);
|
||||
FileUtils.deleteDirectoryRecurse(photoFile);
|
||||
}
|
||||
}
|
||||
finish();
|
||||
});
|
||||
|
||||
final boolean isNewMarker = waypointId == null;
|
||||
isNewMarker = markerId == 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 -> {
|
||||
viewBinding.markerEditDone.setText(isNewMarker ? R.string.generic_add : R.string.generic_save);
|
||||
viewBinding.markerEditDone.setOnClickListener(v -> {
|
||||
if (isNewMarker) {
|
||||
addMarker();
|
||||
} else {
|
||||
saveMarker();
|
||||
}
|
||||
Track.Id trackId = getTrackId();
|
||||
if (trackId == null) {
|
||||
for (Uri photoUri : photoUriDeleteList) {
|
||||
File photoFile = FileUtils.getPhotoFileIfExists(this, trackId, photoUri);
|
||||
FileUtils.deleteDirectoryRecurse(photoFile);
|
||||
}
|
||||
}
|
||||
finish();
|
||||
});
|
||||
|
||||
if (isNewMarker) {
|
||||
int nextWaypointNumber = trackId == null ? -1 : new ContentProviderUtils(this).getNextMarkerNumber(trackId);
|
||||
if (nextWaypointNumber == -1) {
|
||||
nextWaypointNumber = 0;
|
||||
int nextMarkerNumber = trackId == null ? -1 : new ContentProviderUtils(this).getNextMarkerNumber(trackId);
|
||||
if (nextMarkerNumber == -1) {
|
||||
nextMarkerNumber = 0;
|
||||
}
|
||||
waypointName.setText(getString(R.string.marker_name_format, nextWaypointNumber));
|
||||
waypointName.selectAll();
|
||||
waypointMarkerType.setText("");
|
||||
waypointDescription.setText("");
|
||||
viewBinding.markerEditName.setText(getString(R.string.marker_name_format, nextMarkerNumber));
|
||||
viewBinding.markerEditName.selectAll();
|
||||
viewBinding.markerEditMarkerType.setText("");
|
||||
viewBinding.markerEditDescription.setText("");
|
||||
} else {
|
||||
waypoint = new ContentProviderUtils(this).getMarker(waypointId);
|
||||
if (waypoint == null) {
|
||||
Log.d(TAG, "waypoint is null");
|
||||
marker = new ContentProviderUtils(this).getMarker(markerId);
|
||||
if (marker == null) {
|
||||
Log.d(TAG, "marker is null");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
waypointName.setText(waypoint.getName());
|
||||
waypointMarkerType.setText(waypoint.getCategory());
|
||||
waypointDescription.setText(waypoint.getDescription());
|
||||
if (waypoint.hasPhoto()) {
|
||||
photoUri = waypoint.getPhotoURI();
|
||||
viewBinding.markerEditName.setText(marker.getName());
|
||||
viewBinding.markerEditMarkerType.setText(marker.getCategory());
|
||||
viewBinding.markerEditDescription.setText(marker.getDescription());
|
||||
if (marker.hasPhoto()) {
|
||||
photoUri = marker.getPhotoURI();
|
||||
}
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
photoUri = savedInstanceState.getParcelable(BUNDLE_PHOTO_URI);
|
||||
if (waypoint != null) {
|
||||
waypoint.setPhotoUrl(photoUri != null ? photoUri.toString() : null);
|
||||
if (marker != null) {
|
||||
marker.setPhotoUrl(photoUri != null ? photoUri.toString() : null);
|
||||
}
|
||||
}
|
||||
if (photoUri != null) {
|
||||
setWaypointImageView(photoUri);
|
||||
setMarkerImageView(photoUri);
|
||||
}
|
||||
|
||||
hideAndShowOptions();
|
||||
@@ -182,8 +208,9 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.marker_edit;
|
||||
protected View getRootView() {
|
||||
viewBinding = MarkerEditBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -202,10 +229,10 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.marker_edit_insert_photo:
|
||||
createWaypointWithPicture();
|
||||
createMarkerWithPicture();
|
||||
return true;
|
||||
case R.id.marker_edit_insert_gallery_img:
|
||||
createWaypointWithGalleryImage();
|
||||
createMarkerWithGalleryImage();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
@@ -219,7 +246,7 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
} else if (resultCode == RESULT_OK) {
|
||||
setWaypointImageView(photoUri);
|
||||
setMarkerImageView(photoUri);
|
||||
}
|
||||
} else if (requestCode == GALLERY_IMG_REQUEST_CODE) {
|
||||
if (resultCode == RESULT_CANCELED) {
|
||||
@@ -233,7 +260,7 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
FileUtils.copy(srcFd, dstFile);
|
||||
|
||||
photoUri = FileUtils.getUriForFile(this, dstFile);
|
||||
setWaypointImageView(photoUri);
|
||||
setMarkerImageView(photoUri);
|
||||
} catch(Exception e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
Toast.makeText(this, R.string.marker_add_canceled, Toast.LENGTH_LONG).show();
|
||||
@@ -250,31 +277,26 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
* If a photo is set then one's options are shown, otherwise another ones are shown.
|
||||
*/
|
||||
private void hideAndShowOptions() {
|
||||
boolean isPhotoSet = (waypoint != null && waypoint.hasPhoto()) || photoUri != null;
|
||||
boolean isPhotoSet = (marker != null && marker.hasPhoto()) || photoUri != null;
|
||||
if (insertPhotoMenuItem != null && insertGalleryImgMenuItem != null) {
|
||||
insertPhotoMenuItem.setVisible(!isPhotoSet);
|
||||
insertGalleryImgMenuItem.setVisible(!isPhotoSet);
|
||||
}
|
||||
waypointDeletePhotoBtn.setVisibility(isPhotoSet ? View.VISIBLE : View.GONE);
|
||||
viewBinding.markerEditPhotoDelete.setVisibility(isPhotoSet ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the trackId. If trackId == -1 then get it from waypoint object.
|
||||
* Returns the trackId; either from track or marker.
|
||||
*/
|
||||
private Track.Id getTrackId() {
|
||||
return trackId == null ? waypoint.getTrackId() : trackId;
|
||||
return trackId == null ? marker.getTrackId() : trackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ImageView waypointPhoto with the uri photo.
|
||||
*
|
||||
* @param uri the uri photo.
|
||||
*/
|
||||
private void setWaypointImageView(@NonNull Uri uri) {
|
||||
private void setMarkerImageView(@NonNull Uri uri) {
|
||||
try (ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r")) {
|
||||
FileDescriptor fd = pfd.getFileDescriptor();
|
||||
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd);
|
||||
waypointPhoto.setImageBitmap(bitmap);
|
||||
viewBinding.markerEditPhoto.setImageBitmap(bitmap);
|
||||
hideAndShowOptions();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
@@ -282,31 +304,31 @@ public class MarkerEditActivity extends AbstractActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void createWaypointWithPicture() {
|
||||
private void createMarkerWithPicture() {
|
||||
Pair<Intent, Uri> intentAndPhotoUri = IntentUtils.createTakePictureIntent(this, getTrackId());
|
||||
photoUri = intentAndPhotoUri.second;
|
||||
startActivityForResult(intentAndPhotoUri.first, CAMERA_REQUEST_CODE);
|
||||
}
|
||||
|
||||
private void createWaypointWithGalleryImage() {
|
||||
private void createMarkerWithGalleryImage() {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
startActivityForResult(intent, GALLERY_IMG_REQUEST_CODE);
|
||||
}
|
||||
|
||||
private void addMarker() {
|
||||
trackRecordingServiceConnection.addMarker(this,
|
||||
waypointName.getText().toString(),
|
||||
waypointMarkerType.getText().toString(),
|
||||
waypointDescription.getText().toString(),
|
||||
viewBinding.markerEditName.getText().toString(),
|
||||
viewBinding.markerEditMarkerType.getText().toString(),
|
||||
viewBinding.markerEditDescription.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);
|
||||
marker.setName(viewBinding.markerEditName.getText().toString());
|
||||
marker.setCategory(viewBinding.markerEditMarkerType.getText().toString());
|
||||
marker.setDescription(viewBinding.markerEditDescription.getText().toString());
|
||||
marker.setPhotoUrl(photoUri != null ? photoUri.toString() : null);
|
||||
|
||||
new ContentProviderUtils(this).updateMarker(this, waypoint);
|
||||
new ContentProviderUtils(this).updateMarker(this, marker);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -26,18 +27,19 @@ 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.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.CursorLoader;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
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.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;
|
||||
@@ -67,7 +69,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
// Note that the key can be null
|
||||
if (PreferencesUtils.isKey(MarkerListActivity.this, R.string.recording_track_id_key, key)) {
|
||||
recordingTrackId = PreferencesUtils.getRecordingTrackId(MarkerListActivity.this);
|
||||
}
|
||||
@@ -81,8 +82,11 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
};
|
||||
private Track track;
|
||||
private ResourceCursorAdapter resourceCursorAdapter;
|
||||
// UI elements
|
||||
private ListView listView;
|
||||
|
||||
private MarkerListBinding viewBinding;
|
||||
|
||||
private final MarkerLoaderCallback loaderCallbacks = new MarkerLoaderCallback();
|
||||
|
||||
// Callback when an item is selected in the contextual action mode
|
||||
private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() {
|
||||
@Override
|
||||
@@ -120,9 +124,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
|
||||
track = trackId != null ? contentProviderUtils.getTrack(trackId) : null;
|
||||
|
||||
listView = findViewById(R.id.marker_list);
|
||||
listView.setEmptyView(findViewById(R.id.marker_list_empty));
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||
viewBinding.markerList.setEmptyView(viewBinding.markerListEmpty);
|
||||
viewBinding.markerList.setOnItemClickListener((parent, view, position, id) -> {
|
||||
Intent intent = IntentUtils.newIntent(MarkerListActivity.this, MarkerDetailActivity.class)
|
||||
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, new Marker.Id(id));
|
||||
startActivity(intent);
|
||||
@@ -135,8 +138,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
int categoryIndex = cursor.getColumnIndex(MarkerColumns.CATEGORY);
|
||||
int descriptionIndex = cursor.getColumnIndex(MarkerColumns.DESCRIPTION);
|
||||
int photoUrlIndex = cursor.getColumnIndex(MarkerColumns.PHOTOURL);
|
||||
int latitudeIndex = cursor.getColumnIndex(MarkerColumns.LATITUDE);
|
||||
int longitudeIndex = cursor.getColumnIndex(MarkerColumns.LONGITUDE);
|
||||
|
||||
int iconId = MarkerUtils.ICON_ID;
|
||||
String name = cursor.getString(nameIndex);
|
||||
@@ -144,33 +145,14 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
String category = cursor.getString(categoryIndex);
|
||||
String description = cursor.getString(descriptionIndex);
|
||||
String photoUrl = cursor.getString(photoUrlIndex);
|
||||
//TODO also show latitude and longitude in list
|
||||
double latitude = cursor.getDouble(latitudeIndex);
|
||||
double longitude = cursor.getDouble(longitudeIndex);
|
||||
|
||||
ListItemUtils.setListItem(MarkerListActivity.this, view, false, true, iconId, R.string.image_marker, name, null, null, 0, time, false, category, description, photoUrl);
|
||||
}
|
||||
};
|
||||
listView.setAdapter(resourceCursorAdapter);
|
||||
ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
|
||||
viewBinding.markerList.setAdapter(resourceCursorAdapter);
|
||||
ActivityUtils.configureListViewContextualMenu(viewBinding.markerList, contextualActionModeCallback);
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(0, null, new LoaderCallbacks<Cursor>() {
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
|
||||
return ContentProviderUtils.getMarkersLoader(MarkerListActivity.this, track != null ? track.getId() : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
|
||||
resourceCursorAdapter.swapCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
resourceCursorAdapter.swapCursor(null);
|
||||
}
|
||||
});
|
||||
loadData(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -193,8 +175,9 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.marker_list;
|
||||
protected View getRootView() {
|
||||
viewBinding = MarkerListBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -229,39 +212,36 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
/**
|
||||
* Handles a context item selection.
|
||||
*
|
||||
* @param itemId the menu item id
|
||||
* @param longWaypointIds the marker ids
|
||||
* @param itemId the menu item id
|
||||
* @param longMarkerIds the marker ids
|
||||
* @return true if handled.
|
||||
*/
|
||||
private boolean handleContextItem(int itemId, long... longWaypointIds) {
|
||||
Marker.Id[] waypointIds = new Marker.Id[longWaypointIds.length];
|
||||
for (int i = 0; i < longWaypointIds.length; i++) {
|
||||
waypointIds[i] = new Marker.Id(longWaypointIds[i]);
|
||||
private boolean handleContextItem(int itemId, long... longMarkerIds) {
|
||||
Marker.Id[] markerIds = new Marker.Id[longMarkerIds.length];
|
||||
for (int i = 0; i < longMarkerIds.length; i++) {
|
||||
markerIds[i] = new Marker.Id(longMarkerIds[i]);
|
||||
}
|
||||
|
||||
Intent intent;
|
||||
switch (itemId) {
|
||||
case R.id.list_context_menu_show_on_map:
|
||||
if (waypointIds.length == 1) {
|
||||
IntentUtils.showCoordinateOnMap(this, contentProviderUtils.getMarker(waypointIds[0]));
|
||||
if (markerIds.length == 1) {
|
||||
IntentUtils.showCoordinateOnMap(this, contentProviderUtils.getMarker(markerIds[0]));
|
||||
}
|
||||
return true;
|
||||
case R.id.list_context_menu_edit:
|
||||
if (waypointIds.length == 1) {
|
||||
if (markerIds.length == 1) {
|
||||
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
|
||||
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, waypointIds[0]);
|
||||
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerIds[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);
|
||||
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), markerIds);
|
||||
return true;
|
||||
case R.id.list_context_menu_select_all:
|
||||
for (int i = 0; i < listView.getCount(); i++) {
|
||||
listView.setItemChecked(i, true);
|
||||
for (int i = 0; i < viewBinding.markerList.getCount(); i++) {
|
||||
viewBinding.markerList.setItemChecked(i, true);
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
@@ -278,7 +258,92 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteMarkerDone() {
|
||||
public void onBackPressed() {
|
||||
if (loaderCallbacks.getSearchQuery() != null) {
|
||||
loaderCallbacks.setSearch(null);
|
||||
return;
|
||||
}
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
if (!searchView.isIconified()) {
|
||||
searchView.setIconified(true);
|
||||
return;
|
||||
}
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
loadData(intent);
|
||||
}
|
||||
|
||||
private void loadData(Intent intent) {
|
||||
String searchQuery = null;
|
||||
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
||||
searchQuery = intent.getStringExtra(SearchManager.QUERY);
|
||||
}
|
||||
|
||||
loaderCallbacks.setSearch(searchQuery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMarkerDeleted() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
private class MarkerLoaderCallback implements LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
private String searchQuery = null;
|
||||
|
||||
public String getSearchQuery() {
|
||||
return searchQuery;
|
||||
}
|
||||
|
||||
public void setSearch(String searchQuery) {
|
||||
this.searchQuery = searchQuery;
|
||||
restart();
|
||||
if (searchQuery != null) {
|
||||
setTitle(searchQuery);
|
||||
} else {
|
||||
setTitle(R.string.menu_markers);
|
||||
}
|
||||
}
|
||||
|
||||
public void restart() {
|
||||
LoaderManager.getInstance(MarkerListActivity.this).restartLoader(0, null, loaderCallbacks);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
|
||||
final String[] PROJECTION = new String[]{MarkerColumns._ID,
|
||||
MarkerColumns.NAME, MarkerColumns.DESCRIPTION, MarkerColumns.CATEGORY,
|
||||
MarkerColumns.TIME, MarkerColumns.PHOTOURL};
|
||||
|
||||
if (searchQuery == null) {
|
||||
if (track != null) {
|
||||
return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, MarkerColumns.TRACKID + "=?", new String[]{String.valueOf(track.getId().getId())}, null);
|
||||
} else {
|
||||
return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, null, null, null);
|
||||
}
|
||||
} else {
|
||||
final String SEARCH_QUERY = MarkerColumns.NAME + " LIKE ? OR " +
|
||||
MarkerColumns.DESCRIPTION + " LIKE ? OR " +
|
||||
MarkerColumns.CATEGORY + " LIKE ?";
|
||||
final String[] selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"};
|
||||
return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, SEARCH_QUERY, selectionArgs, MarkerColumns.DEFAULT_SORT_ORDER + " DESC");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
|
||||
resourceCursorAdapter.swapCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
resourceCursorAdapter.swapCursor(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,450 +0,0 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
import 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.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
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);
|
||||
Marker.Id markerId = (Marker.Id) item.get(MARKER_ID_FIELD);
|
||||
Intent intent = IntentUtils.newIntent(SearchListActivity.this, TrackRecordedActivity.class);
|
||||
if (markerId != null) {
|
||||
intent = intent.putExtra(TrackRecordedActivity.EXTRA_MARKER_ID, markerId);
|
||||
} else {
|
||||
intent = intent.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
|
||||
}
|
||||
startActivity(intent);
|
||||
});
|
||||
ActivityUtils.configureListViewContextualMenu(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);
|
||||
Marker.Id waypointId = (Marker.Id) item.get(MARKER_ID_FIELD);
|
||||
Intent intent;
|
||||
switch (itemId) {
|
||||
case R.id.list_context_menu_show_on_map:
|
||||
IntentUtils.showCoordinateOnMap(this, (double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + "");
|
||||
return true;
|
||||
case R.id.list_context_menu_share:
|
||||
intent = IntentUtils.newShareFileIntent(this, trackId);
|
||||
intent = Intent.createChooser(intent, null);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.list_context_menu_edit:
|
||||
if (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.marker));
|
||||
}
|
||||
}
|
||||
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(Marker 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.getMarkerCount(track.getId()));
|
||||
resultMap.put(START_TIME_FIELD, trackStatistics.getStartTime_ms());
|
||||
resultMap.put(CATEGORY_FIELD, category);
|
||||
resultMap.put(DESCRIPTION_FIELD, track.getDescription());
|
||||
resultMap.put(PHOTO_URL_FIELD, null);
|
||||
resultMap.put(TRACK_ID_FIELD, track.getId());
|
||||
resultMap.put(MARKER_ID_FIELD, null);
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteMarkerDone() {
|
||||
runOnUiThread(() -> handleIntent(getIntent()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
|
||||
return trackRecordingServiceConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDeleted() {
|
||||
runOnUiThread(() -> handleIntent(getIntent()));
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import android.util.Log;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.util.ActivityUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
|
||||
/**
|
||||
@@ -32,6 +33,8 @@ 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,19 +18,14 @@ package de.dennisguse.opentracks;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.graphics.drawable.AnimatedVectorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.TransitionDrawable;
|
||||
import android.os.Handler;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.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;
|
||||
@@ -42,137 +37,132 @@ import de.dennisguse.opentracks.util.UnitConversions;
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class TrackController {
|
||||
public class TrackController implements View.OnTouchListener {
|
||||
|
||||
private static final String TAG = TrackController.class.getSimpleName();
|
||||
|
||||
private final Activity activity;
|
||||
private final TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private final Handler handlerUpdateTotalTime;
|
||||
private final View containerView;
|
||||
private final TextView statusTextView;
|
||||
private final TextView totalTimeTextView;
|
||||
private final ImageButton recordImageButton;
|
||||
private final ImageButton stopImageButton;
|
||||
private final Handler handlerUpdateTotalTime = new Handler();
|
||||
private final boolean alwaysShow;
|
||||
private ButtonDelay buttonDelay;
|
||||
|
||||
private Runnable buttonDelay;
|
||||
private final int buttonDelayDuration;
|
||||
|
||||
private final TrackControllerBinding viewBinding;
|
||||
private final TransitionDrawable transition;
|
||||
|
||||
private boolean isRecording;
|
||||
private boolean isPaused;
|
||||
private long totalTime = 0;
|
||||
|
||||
private long totalTimeTimestamp = 0;
|
||||
|
||||
private boolean isResumed = false;
|
||||
|
||||
private final Callback callback;
|
||||
|
||||
// A runnable to update the total time.
|
||||
private final Runnable updateTotalTimeRunnable = new Runnable() {
|
||||
public void run() {
|
||||
if (isResumed && isRecording && !isPaused) {
|
||||
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(System.currentTimeMillis() - totalTimeTimestamp + totalTime));
|
||||
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(System.currentTimeMillis() - totalTimeTimestamp + totalTime));
|
||||
handlerUpdateTotalTime.postDelayed(this, UnitConversions.ONE_SECOND_MS);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
TrackController(Activity activity, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, OnClickListener recordListener, OnClickListener stopListener) {
|
||||
TrackController(Activity activity, TrackControllerBinding viewBinding, TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow, Callback callback) {
|
||||
this.activity = activity;
|
||||
this.viewBinding = viewBinding;
|
||||
this.trackRecordingServiceConnection = trackRecordingServiceConnection;
|
||||
this.alwaysShow = alwaysShow;
|
||||
handlerUpdateTotalTime = new Handler();
|
||||
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);
|
||||
this.callback = callback;
|
||||
|
||||
recordImageButton = activity.findViewById(R.id.track_controller_record);
|
||||
recordImageButton.setOnTouchListener((view, motionEvent) -> onRecordTouch(activity, recordListener, motionEvent));
|
||||
buttonDelayDuration = activity.getResources().getInteger(R.integer.buttonDelayMillis);
|
||||
|
||||
stopImageButton = activity.findViewById(R.id.track_controller_stop);
|
||||
stopImageButton.setOnTouchListener((view, motionEvent) -> onStopTouch(activity, stopListener, 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();
|
||||
}
|
||||
|
||||
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);
|
||||
@SuppressLint("ClickableViewAccessibility") //We do accessibility manually.
|
||||
@Override
|
||||
public boolean onTouch(final View view, MotionEvent event) {
|
||||
if (event.getAction() == MotionEvent.ACTION_MOVE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buttonDelay != null && event.getAction() == MotionEvent.ACTION_UP) {
|
||||
handlerUpdateTotalTime.removeCallbacks(buttonDelay);
|
||||
buttonDelay = null;
|
||||
((ImageButton) view).setPressed(false);
|
||||
|
||||
transition.resetTransition();
|
||||
|
||||
showStatusSetDefaultText();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//Trigger button pressed animation
|
||||
((ImageButton) view).setPressed(true);
|
||||
|
||||
//To pause a recording
|
||||
if (viewBinding.trackControllerRecord.equals(view)
|
||||
&& event.getAction() == MotionEvent.ACTION_DOWN
|
||||
&& isRecording && !isPaused) {
|
||||
|
||||
transition.startTransition(buttonDelayDuration);
|
||||
|
||||
ActivityUtils.vibrate(activity, 150);
|
||||
viewBinding.trackControllerStatus.setText(R.string.hold_to_pause);
|
||||
|
||||
buttonDelay = () -> {
|
||||
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||
view.performClick();
|
||||
callback.recordStart(); //TODO Should be recordPause();
|
||||
|
||||
transition.resetTransition();
|
||||
|
||||
ActivityUtils.vibrate(activity, 1000);
|
||||
};
|
||||
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
|
||||
return true;
|
||||
}
|
||||
|
||||
//To stop a recording
|
||||
if (viewBinding.trackControllerStop.equals(view)
|
||||
&& event.getAction() == MotionEvent.ACTION_DOWN
|
||||
&& isRecording) {
|
||||
|
||||
|
||||
transition.startTransition(buttonDelayDuration);
|
||||
|
||||
ActivityUtils.vibrate(activity, 150);
|
||||
viewBinding.trackControllerStatus.setText(R.string.hold_to_stop);
|
||||
|
||||
buttonDelay = () -> {
|
||||
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||
callback.recordStop();
|
||||
ActivityUtils.vibrate(activity, 1000);
|
||||
|
||||
transition.resetTransition();
|
||||
};
|
||||
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean onStopTouch(final Activity activity, final OnClickListener stopListener, final MotionEvent motionEvent) {
|
||||
if (isRecording) {
|
||||
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
buttonDelay = new ButtonDelay(activity, 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;
|
||||
@@ -180,30 +170,29 @@ public class TrackController {
|
||||
isRecording = recording;
|
||||
isPaused = paused;
|
||||
boolean visible = alwaysShow || isRecording;
|
||||
containerView.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
viewBinding.trackControllerContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (!visible) {
|
||||
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
|
||||
return;
|
||||
}
|
||||
|
||||
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.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));
|
||||
|
||||
stopImageButton.setImageResource(isRecording ? R.drawable.ic_button_stop : R.drawable.ic_button_stop_disabled);
|
||||
stopImageButton.setEnabled(isRecording);
|
||||
viewBinding.trackControllerStop.setEnabled(isRecording);
|
||||
|
||||
statusTextView.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
|
||||
viewBinding.trackControllerStatus.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
|
||||
if (isRecording) {
|
||||
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);
|
||||
viewBinding.trackControllerStatus.setTextColor(activity.getResources().getColor(isPaused ? android.R.color.white : R.color.recording_text));
|
||||
showStatusSetDefaultText();
|
||||
}
|
||||
|
||||
handlerUpdateTotalTime.removeCallbacks(updateTotalTimeRunnable);
|
||||
totalTimeTextView.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
|
||||
viewBinding.trackControllerTotalTime.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
|
||||
if (isRecording) {
|
||||
totalTime = getTotalTime();
|
||||
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
|
||||
viewBinding.trackControllerTotalTime.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
|
||||
if (!isPaused) {
|
||||
totalTimeTimestamp = System.currentTimeMillis();
|
||||
handlerUpdateTotalTime.postDelayed(updateTotalTimeRunnable, UnitConversions.ONE_SECOND_MS);
|
||||
@@ -222,11 +211,11 @@ public class TrackController {
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
containerView.setVisibility(View.GONE);
|
||||
viewBinding.trackControllerContainer.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void show() {
|
||||
containerView.setVisibility(View.VISIBLE);
|
||||
viewBinding.trackControllerContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,4 +225,14 @@ public class TrackController {
|
||||
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
|
||||
return trackRecordingService != null ? trackRecordingService.getTotalTime() : 0L;
|
||||
}
|
||||
|
||||
private void showStatusSetDefaultText() {
|
||||
viewBinding.trackControllerStatus.setText(isPaused ? R.string.generic_paused : R.string.generic_recording);
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void recordStart();
|
||||
|
||||
void recordStop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -40,6 +41,8 @@ public class TrackDeleteActivity extends AbstractActivity {
|
||||
|
||||
private Thread deleteThread;
|
||||
|
||||
private TrackDeleteBinding viewBinding;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -52,13 +55,7 @@ public class TrackDeleteActivity extends AbstractActivity {
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(TrackDeleteActivity.this);
|
||||
|
||||
PowerManager.WakeLock wakeLock = SystemUtils.acquireWakeLock(TrackDeleteActivity.this, null);
|
||||
|
||||
for (Track.Id id : trackIds) {
|
||||
if (Thread.interrupted()) {
|
||||
break;
|
||||
}
|
||||
contentProviderUtils.deleteTrack(TrackDeleteActivity.this, id);
|
||||
}
|
||||
contentProviderUtils.deleteTracks(TrackDeleteActivity.this, trackIds);
|
||||
|
||||
wakeLock = SystemUtils.releaseWakeLock(wakeLock);
|
||||
if (Thread.interrupted()) {
|
||||
@@ -82,12 +79,13 @@ public class TrackDeleteActivity extends AbstractActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.track_delete;
|
||||
protected View getRootView() {
|
||||
viewBinding = TrackDeleteBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
public void onAsyncTaskCompleted() {
|
||||
findViewById(R.id.progressbar).setVisibility(View.INVISIBLE);
|
||||
viewBinding.progressbar.setVisibility(View.INVISIBLE);
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
|
||||
@@ -18,19 +18,14 @@ 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;
|
||||
@@ -55,10 +50,7 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
||||
private Track track;
|
||||
private String iconValue;
|
||||
|
||||
private EditText nameEditText;
|
||||
private AutoCompleteTextView activityType;
|
||||
private Spinner activityTypeIcon;
|
||||
private EditText description;
|
||||
private TrackEditBinding viewBinding;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
@@ -75,25 +67,22 @@ 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);
|
||||
Log.e(TAG, "No track for " + trackId.getId());
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
nameEditText = findViewById(R.id.track_edit_name);
|
||||
nameEditText.setText(track.getName());
|
||||
viewBinding.trackEditName.setText(track.getName());
|
||||
|
||||
activityType = findViewById(R.id.track_edit_activity_type);
|
||||
activityType.setText(track.getCategory());
|
||||
viewBinding.trackEditActivityType.setText(track.getCategory());
|
||||
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
|
||||
activityType.setAdapter(adapter);
|
||||
activityType.setOnItemClickListener((parent, view, position, id) -> setActivityTypeIcon(TrackIconUtils.getIconValue(
|
||||
TrackEditActivity.this, (String) activityType.getAdapter().getItem(position))));
|
||||
activityType.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
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) -> {
|
||||
if (!hasFocus) {
|
||||
setActivityTypeIcon(TrackIconUtils.getIconValue(
|
||||
TrackEditActivity.this, activityType.getText().toString()));
|
||||
TrackEditActivity.this, viewBinding.trackEditActivityType.getText().toString()));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -105,40 +94,25 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
||||
iconValue = track.getIcon();
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
setActivityTypeIcon(iconValue);
|
||||
viewBinding.trackEditActivityTypeIcon.setOnClickListener(v -> ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), viewBinding.trackEditActivityType.getText().toString()));
|
||||
|
||||
description = findViewById(R.id.track_edit_description);
|
||||
description.setText(track.getDescription());
|
||||
viewBinding.trackEditDescription.setText(track.getDescription());
|
||||
|
||||
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(),
|
||||
viewBinding.trackEditSave.setOnClickListener(v -> {
|
||||
TrackUtils.updateTrack(TrackEditActivity.this, track, viewBinding.trackEditName.getText().toString(),
|
||||
viewBinding.trackEditActivityType.getText().toString(), viewBinding.trackEditDescription.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);
|
||||
cancel.setVisibility(View.GONE);
|
||||
viewBinding.trackEditCancel.setVisibility(View.GONE);
|
||||
} else {
|
||||
setTitle(R.string.menu_edit);
|
||||
cancel.setOnClickListener(v -> finish());
|
||||
cancel.setVisibility(View.VISIBLE);
|
||||
viewBinding.trackEditCancel.setOnClickListener(v -> finish());
|
||||
viewBinding.trackEditCancel.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,18 +136,19 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.track_edit;
|
||||
protected View getRootView() {
|
||||
viewBinding = TrackEditBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
private void setActivityTypeIcon(String value) {
|
||||
iconValue = value;
|
||||
TrackIconUtils.setIconSpinner(activityTypeIcon, value);
|
||||
private void setActivityTypeIcon(String iconValue) {
|
||||
this.iconValue = iconValue;
|
||||
viewBinding.trackEditActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(iconValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChooseActivityTypeDone(String value) {
|
||||
setActivityTypeIcon(value);
|
||||
activityType.setText(getString(TrackIconUtils.getIconActivityType(value)));
|
||||
viewBinding.trackEditActivityType.setText(getString(TrackIconUtils.getIconActivityType(value)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,6 @@ import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -42,12 +40,12 @@ import androidx.appcompat.widget.SearchView;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.cursoradapter.widget.ResourceCursorAdapter;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.CursorLoader;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.databinding.TrackListBinding;
|
||||
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||
@@ -66,35 +64,20 @@ import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller {
|
||||
public class TrackListActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackController.Callback {
|
||||
|
||||
private static final String TAG = TrackListActivity.class.getSimpleName();
|
||||
|
||||
// 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 final LoaderCallbacks<Cursor> loaderCallbacks = new LoaderCallbacks<Cursor>() {
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
|
||||
return ContentProviderUtils.getTracksCursorLoader(TrackListActivity.this);
|
||||
}
|
||||
private TrackListBinding viewBinding;
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
|
||||
resourceCursorAdapter.swapCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
resourceCursorAdapter.swapCursor(null);
|
||||
}
|
||||
};
|
||||
private final TrackLoaderCallBack loaderCallbacks = new TrackLoaderCallBack();
|
||||
|
||||
// Preferences
|
||||
private boolean metricUnits = true;
|
||||
@@ -112,8 +95,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onClick(int itemId, int[] positions, long[] ids) {
|
||||
return handleContextItem(itemId, ids);
|
||||
public boolean onClick(int itemId, int[] positions, long[] trackIds) {
|
||||
return handleContextItem(itemId, trackIds);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -137,7 +120,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
if (key != null) {
|
||||
runOnUiThread(() -> {
|
||||
TrackListActivity.this.invalidateOptionsMenu();
|
||||
LoaderManager.getInstance(TrackListActivity.this).restartLoader(0, null, loaderCallbacks);
|
||||
loaderCallbacks.restart();
|
||||
boolean isRecording = PreferencesUtils.isRecording(recordingTrackId);
|
||||
trackController.update(isRecording, recordingTrackPaused);
|
||||
});
|
||||
@@ -149,14 +132,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
private MenuItem searchMenuItem;
|
||||
private MenuItem startGpsMenuItem;
|
||||
|
||||
private final OnClickListener stopListener = new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
updateMenuItems(false, false);
|
||||
trackRecordingServiceConnection.stopRecording(TrackListActivity.this, true);
|
||||
}
|
||||
};
|
||||
|
||||
// Callback when the trackRecordingServiceConnection binding changes.
|
||||
private final Runnable bindChangedCallback = new Runnable() {
|
||||
@Override
|
||||
@@ -191,42 +166,23 @@ 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, trackRecordingServiceConnection, true, recordListener, stopListener);
|
||||
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, true, this);
|
||||
|
||||
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
||||
|
||||
// Show trackController when search dialog is dismissed
|
||||
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
|
||||
@@ -234,9 +190,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
searchManager.setOnDismissListener(() -> trackController.show());
|
||||
}
|
||||
|
||||
listView = findViewById(R.id.track_list);
|
||||
listView.setEmptyView(findViewById(R.id.track_list_empty_view));
|
||||
listView.setOnItemClickListener((parent, view, position, trackId) -> {
|
||||
viewBinding.trackList.setEmptyView(viewBinding.trackListEmptyView);
|
||||
viewBinding.trackList.setOnItemClickListener((parent, view, position, trackId) -> {
|
||||
Intent newIntent;
|
||||
if (trackId == recordingTrackId.getId()) {
|
||||
// Is recording -> open record activity.
|
||||
@@ -253,14 +208,15 @@ 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.getColumnIndex(TracksColumns._ID);
|
||||
int iconIndex = cursor.getColumnIndex(TracksColumns.ICON);
|
||||
int nameIndex = cursor.getColumnIndex(TracksColumns.NAME);
|
||||
int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID);
|
||||
int iconIndex = cursor.getColumnIndexOrThrow(TracksColumns.ICON);
|
||||
int nameIndex = cursor.getColumnIndexOrThrow(TracksColumns.NAME);
|
||||
int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
|
||||
int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
|
||||
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
|
||||
int categoryIndex = cursor.getColumnIndex(TracksColumns.CATEGORY);
|
||||
int descriptionIndex = cursor.getColumnIndex(TracksColumns.DESCRIPTION);
|
||||
int categoryIndex = cursor.getColumnIndexOrThrow(TracksColumns.CATEGORY);
|
||||
int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION);
|
||||
int markerCountIndex = cursor.getColumnIndexOrThrow(TracksColumns.MARKER_COUNT);
|
||||
|
||||
Track.Id trackId = new Track.Id(cursor.getLong(idIndex));
|
||||
boolean isRecording = trackId.equals(recordingTrackId);
|
||||
@@ -269,7 +225,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
String name = cursor.getString(nameIndex);
|
||||
String totalTime = StringUtils.formatElapsedTime(cursor.getLong(totalTimeIndex));
|
||||
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, cursor.getDouble(totalDistanceIndex), metricUnits);
|
||||
int markerCount = contentProviderUtils.getMarkerCount(trackId);
|
||||
int markerCount = cursor.getInt(markerCountIndex);
|
||||
long startTime = cursor.getLong(startTimeIndex);
|
||||
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
|
||||
String description = cursor.getString(descriptionIndex);
|
||||
@@ -279,11 +235,10 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
startTime, true, category, description, null);
|
||||
}
|
||||
};
|
||||
listView.setAdapter(resourceCursorAdapter);
|
||||
viewBinding.trackList.setAdapter(resourceCursorAdapter);
|
||||
ActivityUtils.configureListViewContextualMenu(viewBinding.trackList, contextualActionModeCallback);
|
||||
|
||||
ActivityUtils.configureListViewContextualMenu(listView, contextualActionModeCallback);
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(0, null, loaderCallbacks);
|
||||
loadData(getIntent());
|
||||
|
||||
requestGPSPermissions();
|
||||
}
|
||||
@@ -297,6 +252,14 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
trackRecordingServiceConnection.startConnection(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
// Update UI
|
||||
trackController.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
@@ -307,14 +270,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
trackController.onResume(PreferencesUtils.isRecording(recordingTrackId), recordingTrackPaused);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
// Update UI
|
||||
trackController.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
@@ -335,8 +290,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.track_list;
|
||||
protected View getRootView() {
|
||||
viewBinding = TrackListBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -412,6 +368,36 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (loaderCallbacks.getSearchQuery() != null) {
|
||||
loaderCallbacks.setSearch(null);
|
||||
return;
|
||||
}
|
||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||
if (!searchView.isIconified()) {
|
||||
searchView.setIconified(true);
|
||||
return;
|
||||
}
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
loadData(intent);
|
||||
}
|
||||
|
||||
private void loadData(Intent intent) {
|
||||
String searchQuery = null;
|
||||
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
||||
searchQuery = intent.getStringExtra(SearchManager.QUERY);
|
||||
}
|
||||
|
||||
loaderCallbacks.setSearch(searchQuery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSearchRequested() {
|
||||
// Hide trackController when search dialog is shown
|
||||
@@ -425,7 +411,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDeleted() {
|
||||
protected void onTrackDeleted() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@@ -500,12 +486,91 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
|
||||
deleteTracks(trackIds);
|
||||
return true;
|
||||
case R.id.list_context_menu_select_all:
|
||||
int size = listView.getCount();
|
||||
int size = viewBinding.trackList.getCount();
|
||||
for (int i = 0; i < size; i++) {
|
||||
listView.setItemChecked(i, true);
|
||||
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,31 +21,27 @@ 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.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import de.dennisguse.opentracks.content.TrackDataHub;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
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.IntervalsRecordedFragment;
|
||||
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.
|
||||
@@ -53,21 +49,22 @@ import de.dennisguse.opentracks.util.TrackUtils;
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackRecordedActivity extends AbstractListActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
|
||||
public class TrackRecordedActivity extends AbstractListActivity implements ConfirmDeleteDialogFragment.ConfirmDeleteCaller, TrackActivityDataHubInterface {
|
||||
|
||||
private static final String TAG = TrackRecordedActivity.class.getSimpleName();
|
||||
|
||||
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 Track track;
|
||||
private TrackRecordedBinding viewBinding;
|
||||
|
||||
private Track.Id trackId;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -78,12 +75,10 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
|
||||
|
||||
trackDataHub = new TrackDataHub(this);
|
||||
|
||||
pager = findViewById(R.id.track_detail_activity_view_pager);
|
||||
pager.setAdapter(new CustomFragmentPagerAdapter(getSupportFragmentManager(), FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT));
|
||||
TabLayout tabs = findViewById(R.id.track_detail_activity_tablayout);
|
||||
tabs.setupWithViewPager(pager);
|
||||
viewBinding.trackDetailActivityViewPager.setAdapter(new CustomFragmentPagerAdapter(getSupportFragmentManager(), FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT));
|
||||
viewBinding.trackDetailActivityTablayout.setupWithViewPager(viewBinding.trackDetailActivityViewPager);
|
||||
if (savedInstanceState != null) {
|
||||
pager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
|
||||
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,8 +95,8 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
|
||||
// Update UI
|
||||
this.invalidateOptionsMenu();
|
||||
|
||||
if (track != null) {
|
||||
trackDataHub.loadTrack(track.getId());
|
||||
if (trackId != null) {
|
||||
trackDataHub.loadTrack(trackId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,12 +109,13 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt(CURRENT_TAB_TAG_KEY, pager.getCurrentItem());
|
||||
outState.putInt(CURRENT_TAB_TAG_KEY, viewBinding.trackDetailActivityViewPager.getCurrentItem());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.track_detail;
|
||||
protected View getRootView() {
|
||||
viewBinding = TrackRecordedBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -139,6 +135,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
menu.findItem(R.id.track_detail_markers).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
menu.findItem(R.id.track_detail_resume_track).setVisible(!PreferencesUtils.isRecording(this));
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
setTitle(track != null ? track.getName() : "");
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
@@ -148,29 +145,29 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
|
||||
Intent intent;
|
||||
switch (item.getItemId()) {
|
||||
case R.id.track_detail_share:
|
||||
intent = IntentUtils.newShareFileIntent(this, track.getId());
|
||||
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, track.getId());
|
||||
IntentDashboardUtils.startDashboard(this, false, trackId);
|
||||
return true;
|
||||
case R.id.track_detail_markers:
|
||||
intent = IntentUtils.newIntent(this, MarkerListActivity.class)
|
||||
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, track.getId());
|
||||
.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, track.getId());
|
||||
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.track_detail_delete:
|
||||
deleteTracks(track.getId());
|
||||
deleteTracks(trackId);
|
||||
return true;
|
||||
case R.id.track_detail_resume_track:
|
||||
Intent newIntent = IntentUtils.newIntent(TrackRecordedActivity.this, TrackRecordingActivity.class)
|
||||
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, track.getId());
|
||||
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(newIntent);
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||
finish();
|
||||
@@ -191,7 +188,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDeleted() {
|
||||
protected void onTrackDeleted() {
|
||||
runOnUiThread(this::finish);
|
||||
}
|
||||
|
||||
@@ -204,35 +201,23 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
Track.Id trackId = intent.getParcelableExtra(EXTRA_TRACK_ID);
|
||||
trackId = intent.getParcelableExtra(EXTRA_TRACK_ID);
|
||||
|
||||
Marker.Id waypointId = intent.getParcelableExtra(EXTRA_MARKER_ID);
|
||||
if (waypointId != null) {
|
||||
Marker.Id markerId = intent.getParcelableExtra(EXTRA_MARKER_ID);
|
||||
if (markerId != null) {
|
||||
// Use the trackId from the marker
|
||||
Marker waypoint = contentProviderUtils.getMarker(waypointId);
|
||||
if (waypoint == null) {
|
||||
Marker marker = contentProviderUtils.getMarker(markerId);
|
||||
if (marker == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
trackId = waypoint.getTrackId();
|
||||
trackId = marker.getTrackId();
|
||||
}
|
||||
|
||||
if (trackId == null) {
|
||||
Log.e(TAG, "TrackDetailActivity needs EXTRA_TRACK_ID.");
|
||||
finish();
|
||||
}
|
||||
|
||||
track = contentProviderUtils.getTrack(trackId);
|
||||
}
|
||||
|
||||
public void chooseActivityType(String category) {
|
||||
ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), category);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChooseActivityTypeDone(String iconValue) {
|
||||
String category = getString(TrackIconUtils.getIconActivityType(iconValue));
|
||||
TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils);
|
||||
}
|
||||
|
||||
private class CustomFragmentPagerAdapter extends FragmentPagerAdapter {
|
||||
@@ -251,9 +236,9 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
|
||||
public Fragment getItem(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return StatisticsRecordedFragment.newInstance(track.getId());
|
||||
return StatisticsRecordedFragment.newInstance(trackId);
|
||||
case 1:
|
||||
return IntervalsRecordedFragment.newInstance(track.getId());
|
||||
return IntervalsFragment.newInstance();
|
||||
case 2:
|
||||
return ChartFragment.newInstance(false);
|
||||
case 3:
|
||||
|
||||
@@ -9,23 +9,20 @@ import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
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.IntervalsRecordingFragment;
|
||||
import de.dennisguse.opentracks.fragments.IntervalsFragment;
|
||||
import de.dennisguse.opentracks.fragments.StatisticsRecordingFragment;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
|
||||
@@ -44,7 +41,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
|
||||
*/
|
||||
//NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key.
|
||||
//This mode of communication should be removed anyhow.
|
||||
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface {
|
||||
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, TrackController.Callback {
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
|
||||
@@ -57,18 +54,16 @@ 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;
|
||||
|
||||
// Preferences
|
||||
private boolean recordingTrackPaused;
|
||||
|
||||
// Intervals recording fragment needs Track.Id when the activity creates it.
|
||||
private OnTrackIdListener intervalsListener;
|
||||
|
||||
private final Runnable bindChangedCallback = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -85,9 +80,6 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
if (trackId == null) {
|
||||
// trackId isn't initialized -> leads a new recording.
|
||||
trackId = service.startNewTrack();
|
||||
if (intervalsListener != null) {
|
||||
intervalsListener.onTrackId(trackId);
|
||||
}
|
||||
} else {
|
||||
// trackId is initialized -> resumes the track.
|
||||
service.resumeTrack(trackId);
|
||||
@@ -133,41 +125,11 @@ 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.
|
||||
@@ -182,20 +144,17 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
}
|
||||
|
||||
recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this);
|
||||
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback);
|
||||
trackDataHub = new TrackDataHub(this);
|
||||
|
||||
pager = findViewById(R.id.track_detail_activity_view_pager);
|
||||
pager.setAdapter(new CustomFragmentPagerAdapter(getSupportFragmentManager(), FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT));
|
||||
TabLayout tabs = findViewById(R.id.track_detail_activity_tablayout);
|
||||
tabs.setupWithViewPager(pager);
|
||||
viewBinding.trackDetailActivityViewPager.setAdapter(new CustomFragmentPagerAdapter(getSupportFragmentManager(), FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT));
|
||||
viewBinding.trackDetailActivityTablayout.setupWithViewPager(viewBinding.trackDetailActivityViewPager);
|
||||
if (savedInstanceState != null) {
|
||||
pager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
|
||||
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
|
||||
}
|
||||
|
||||
trackController = new TrackController(this, trackRecordingServiceConnection, false, recordListener, stopListener);
|
||||
trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, false, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -273,7 +232,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt(CURRENT_TAB_TAG_KEY, pager.getCurrentItem());
|
||||
outState.putInt(CURRENT_TAB_TAG_KEY, viewBinding.trackDetailActivityViewPager.getCurrentItem());
|
||||
outState.putParcelable(EXTRA_TRACK_ID, trackId);
|
||||
}
|
||||
|
||||
@@ -292,8 +251,9 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.track_record;
|
||||
protected View getRootView() {
|
||||
viewBinding = TrackRecordingBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -372,6 +332,32 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@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 FragmentPagerAdapter {
|
||||
|
||||
public CustomFragmentPagerAdapter(@NonNull FragmentManager fm, int behavior) {
|
||||
@@ -390,7 +376,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
case 0:
|
||||
return StatisticsRecordingFragment.newInstance();
|
||||
case 1:
|
||||
return IntervalsRecordingFragment.newInstance(trackId);
|
||||
return IntervalsFragment.IntervalsRecordingFragment.newInstance();
|
||||
case 2:
|
||||
return ChartFragment.newInstance(false);
|
||||
case 3:
|
||||
@@ -416,12 +402,4 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnTrackIdListener {
|
||||
void onTrackId(Track.Id trackId);
|
||||
}
|
||||
|
||||
public void setTrackIdListener(OnTrackIdListener listener) {
|
||||
this.intervalsListener = listener;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,18 +20,23 @@ import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
|
||||
|
||||
public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.Interval> {
|
||||
|
||||
private StackMode stackMode;
|
||||
private boolean metricUnits;
|
||||
private float sumDistance_m = 0f;
|
||||
private float sumDistance_m;
|
||||
private String category;
|
||||
|
||||
public IntervalStatisticsAdapter(Context context, List<IntervalStatistics.Interval> intervalList) {
|
||||
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) {
|
||||
IntervalStatistics.Interval interval = getItem(position);
|
||||
int actualPosition = stackMode == StackMode.STACK_FROM_TOP ? position : getCount() - 1 - position;
|
||||
IntervalStatistics.Interval interval = getItem(actualPosition);
|
||||
ViewHolder viewHolder;
|
||||
|
||||
if (intervalView == null) {
|
||||
@@ -40,29 +45,46 @@ public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.I
|
||||
intervalView = LayoutInflater.from(getContext()).inflate(R.layout.interval_stats_list_item, parent, false);
|
||||
|
||||
viewHolder.distance = intervalView.findViewById(R.id.interval_item_distance);
|
||||
viewHolder.speed = intervalView.findViewById(R.id.interval_item_speed);
|
||||
viewHolder.pace = intervalView.findViewById(R.id.interval_item_pace);
|
||||
viewHolder.rate = intervalView.findViewById(R.id.interval_item_rate);
|
||||
viewHolder.gain = intervalView.findViewById(R.id.interval_item_gain);
|
||||
|
||||
intervalView.setTag(viewHolder);
|
||||
} else {
|
||||
viewHolder = (ViewHolder) intervalView.getTag();
|
||||
}
|
||||
|
||||
sumDistance_m += interval.getDistance_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));
|
||||
|
||||
Pair<String, String> speedParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed_ms(), metricUnits, true);
|
||||
viewHolder.speed.setText(speedParts.first + " " + speedParts.second);
|
||||
if (PreferencesUtils.isReportSpeed(getContext(), category)) {
|
||||
Pair<String, String> speedParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed_ms(), metricUnits, true);
|
||||
viewHolder.rate.setText(speedParts.first + " " + speedParts.second);
|
||||
} else {
|
||||
Pair<String, String> paceParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed_ms(), metricUnits, false);
|
||||
viewHolder.rate.setText(paceParts.first + " " + paceParts.second);
|
||||
}
|
||||
|
||||
Pair<String, String> paceParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed_ms(), metricUnits, false);
|
||||
viewHolder.pace.setText(paceParts.first + " " + paceParts.second);
|
||||
Pair<String, String> gainParts = StringUtils.formatElevation(getContext(), interval.getGain_m(), metricUnits);
|
||||
viewHolder.gain.setText(gainParts.first + " " + gainParts.second);
|
||||
|
||||
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 speed;
|
||||
private TextView pace;
|
||||
private TextView rate;
|
||||
private TextView gain;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package de.dennisguse.opentracks.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorData;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataCyclingPower;
|
||||
import de.dennisguse.opentracks.content.sensor.SensorDataHeartRate;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
public class SensorsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
public static final int HEART_RATE_TYPE = 0;
|
||||
public static final int CADENCE_TYPE = 1;
|
||||
public static final int POWER_TYPE = 2;
|
||||
|
||||
private List<Pair<Integer, SensorData>> sensorDataList;
|
||||
private final Context context;
|
||||
|
||||
public SensorsAdapter(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.sensor_item, parent, false);
|
||||
return new SensorsAdapter.ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
SensorsAdapter.ViewHolder viewHolder = (SensorsAdapter.ViewHolder) holder;
|
||||
int type = sensorDataList.get(position).first;
|
||||
SensorData sensorData = sensorDataList.get(position).second;
|
||||
viewHolder.setData(sensorData, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (sensorDataList == null) {
|
||||
return 0;
|
||||
} else {
|
||||
return sensorDataList.size();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return sensorDataList.get(position).first;
|
||||
}
|
||||
|
||||
public List<Pair<Integer, SensorData>> swapData(List<Pair<Integer, SensorData>> data) {
|
||||
if (sensorDataList == data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
sensorDataList = data;
|
||||
|
||||
if (data != null) {
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView label;
|
||||
TextView sensorValue;
|
||||
TextView value;
|
||||
TextView unit;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
label = itemView.findViewById(R.id.stats_sensor_label);
|
||||
sensorValue = itemView.findViewById(R.id.stats_sensor_sensor_value);
|
||||
value = itemView.findViewById(R.id.stats_sensor_value);
|
||||
unit = itemView.findViewById(R.id.stats_sensor_unit);
|
||||
}
|
||||
|
||||
public void setData(SensorData sensorData, int type) {
|
||||
switch (type) {
|
||||
case HEART_RATE_TYPE:
|
||||
setHeartRateSensorData((SensorDataHeartRate) sensorData);
|
||||
break;
|
||||
case CADENCE_TYPE:
|
||||
setCadenceSensorData((SensorDataCycling.Cadence) sensorData);
|
||||
break;
|
||||
case POWER_TYPE:
|
||||
setPowerSensorData((SensorDataCyclingPower) sensorData);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unknown sensor type");
|
||||
}
|
||||
}
|
||||
|
||||
private void setHeartRateSensorData(SensorDataHeartRate data) {
|
||||
String sensorValue = context.getString(R.string.value_unknown);
|
||||
String sensorName = context.getString(R.string.value_unknown);
|
||||
if (data != null) {
|
||||
sensorName = data.getSensorNameOrAddress();
|
||||
if (data.hasHeartRate_bpm() && data.isRecent()) {
|
||||
sensorValue = StringUtils.formatDecimal(data.getHeartRate_bpm(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.label.setText(context.getString(R.string.sensor_state_heart_rate));
|
||||
this.sensorValue.setText(sensorName);
|
||||
this.value.setText(sensorValue);
|
||||
this.unit.setText(R.string.sensor_unit_beats_per_minute);
|
||||
}
|
||||
|
||||
private void setCadenceSensorData(SensorDataCycling.Cadence data) {
|
||||
String sensorValue = context.getString(R.string.value_unknown);
|
||||
String sensorName = context.getString(R.string.value_unknown);
|
||||
if (data != null) {
|
||||
sensorName = data.getSensorNameOrAddress();
|
||||
|
||||
if (data.hasCadence_rpm() && data.isRecent()) {
|
||||
sensorValue = StringUtils.formatDecimal(data.getCadence_rpm(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.label.setText(context.getString(R.string.sensor_state_cadence));
|
||||
this.sensorValue.setText(sensorName);
|
||||
this.value.setText(sensorValue);
|
||||
this.unit.setText(R.string.sensor_unit_rounds_per_minute);
|
||||
}
|
||||
|
||||
private void setPowerSensorData(SensorDataCyclingPower data) {
|
||||
String sensorValue = context.getString(R.string.value_unknown);
|
||||
String sensorName = context.getString(R.string.value_unknown);
|
||||
if (data != null) {
|
||||
sensorName = data.getSensorNameOrAddress();
|
||||
|
||||
if (data.hasPower_w() && data.isRecent()) {
|
||||
sensorValue = StringUtils.formatDecimal(data.getPower_w(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.label.setText(context.getString(R.string.sensor_state_power));
|
||||
this.sensorValue.setText(sensorName);
|
||||
this.value.setText(sensorValue);
|
||||
this.unit.setText(R.string.sensor_unit_power);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package de.dennisguse.opentracks.android;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Wraps a {@link ContentResolver} and provides access as {@link IContentResolver}.
|
||||
*/
|
||||
public class ContentResolverWrapper implements IContentResolver {
|
||||
|
||||
private final ContentResolver contentResolver;
|
||||
|
||||
public ContentResolverWrapper(ContentResolver contentResolver) {
|
||||
this.contentResolver = contentResolver;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Uri insert(@NonNull Uri url, @Nullable ContentValues values) {
|
||||
return contentResolver.insert(url, values);
|
||||
}
|
||||
|
||||
public int bulkInsert(@NonNull Uri url, @NonNull ContentValues[] values) {
|
||||
return contentResolver.bulkInsert(url, values);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
|
||||
return contentResolver.query(uri, projection, selection, selectionArgs, sortOrder);
|
||||
}
|
||||
|
||||
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String where, @Nullable String[] selectionArgs) {
|
||||
return contentResolver.update(uri, values, where, selectionArgs);
|
||||
}
|
||||
|
||||
public int delete(@NonNull Uri url, @Nullable String where, @Nullable String[] selectionArgs) {
|
||||
return contentResolver.delete(url, where, selectionArgs);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package de.dennisguse.opentracks.android;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresPermission;
|
||||
|
||||
/**
|
||||
* Defines the interface actually shared by {@link android.content.ContentProvider} and {@link android.content.ContentResolver}.
|
||||
* So, both can be used interchangeably.
|
||||
*/
|
||||
public interface IContentResolver {
|
||||
@Nullable
|
||||
Uri insert(@RequiresPermission.Write @NonNull Uri url, @Nullable ContentValues values);
|
||||
|
||||
int bulkInsert(@RequiresPermission.Write @NonNull Uri url, @NonNull ContentValues[] values);
|
||||
|
||||
@Nullable
|
||||
Cursor query(@RequiresPermission.Read @NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder);
|
||||
|
||||
int update(@RequiresPermission.Write @NonNull Uri uri, @Nullable ContentValues values, @Nullable String where, @Nullable String[] selectionArgs);
|
||||
|
||||
int delete(@RequiresPermission.Write @NonNull Uri url, @Nullable String where, @Nullable String[] selectionArgs);
|
||||
}
|
||||
@@ -47,9 +47,9 @@ abstract class ChartValueSeries {
|
||||
private final Paint strokePaint;
|
||||
private final Paint titlePaint;
|
||||
private final Paint markerPaint;
|
||||
private final ExtremityMonitor extremityMonitor;
|
||||
private final NumberFormat numberFormat;
|
||||
private final Path path;
|
||||
private final ExtremityMonitor extremityMonitor = new ExtremityMonitor();
|
||||
private final NumberFormat numberFormat = NumberFormat.getIntegerInstance();
|
||||
private final Path path = new Path();
|
||||
|
||||
private int interval = 1;
|
||||
private int minMarkerValue = 0;
|
||||
@@ -91,17 +91,15 @@ abstract class ChartValueSeries {
|
||||
titlePaint = new Paint(strokePaint);
|
||||
titlePaint.setTextSize(ChartView.MEDIUM_TEXT_SIZE * scale);
|
||||
titlePaint.setTextAlign(Align.CENTER);
|
||||
titlePaint.setStyle(Style.FILL_AND_STROKE);
|
||||
|
||||
markerPaint = new Paint(strokePaint);
|
||||
markerPaint.setTextSize(ChartView.SMALL_TEXT_SIZE * scale);
|
||||
markerPaint.setTextAlign(Align.RIGHT);
|
||||
markerPaint.setStyle(Style.FILL_AND_STROKE);
|
||||
|
||||
// Set stroke paint thickness
|
||||
strokePaint.setStrokeWidth(STROKE_WIDTH);
|
||||
|
||||
extremityMonitor = new ExtremityMonitor();
|
||||
numberFormat = NumberFormat.getIntegerInstance();
|
||||
path = new Path();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,7 +19,6 @@ package de.dennisguse.opentracks.chart;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.DashPathEffect;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Align;
|
||||
@@ -47,6 +46,7 @@ import de.dennisguse.opentracks.stats.ExtremityMonitor;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.MarkerUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.ThemeUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
/**
|
||||
@@ -75,7 +75,7 @@ public class ChartView extends View {
|
||||
private static final int Y_AXIS_OFFSET = 16;
|
||||
|
||||
//TODO Determine from actual size of the used drawable
|
||||
private static final float WAYPOINT_X_ANCHOR = 13f / 48f;
|
||||
private static final float MARKER_X_ANCHOR = 13f / 48f;
|
||||
|
||||
static {
|
||||
X_FRACTION_FORMAT.setMaximumFractionDigits(1);
|
||||
@@ -87,14 +87,15 @@ public class ChartView extends View {
|
||||
private final ChartValueSeries paceSeries;
|
||||
|
||||
private final List<ChartPoint> chartPoints = new ArrayList<>();
|
||||
private final List<Marker> waypoints = new ArrayList<>();
|
||||
private final List<Marker> markers = new ArrayList<>();
|
||||
private final ExtremityMonitor xExtremityMonitor = new ExtremityMonitor();
|
||||
private final int backgroundColor;
|
||||
private final Paint axisPaint;
|
||||
private final Paint xAxisMarkerPaint;
|
||||
private final Paint gridPaint;
|
||||
private final Paint markerPaint;
|
||||
private final Drawable pointer;
|
||||
private final Drawable waypointMarker;
|
||||
private final Drawable markerPin;
|
||||
private final int markerWidth;
|
||||
private final int markerHeight;
|
||||
private final Scroller scroller;
|
||||
@@ -150,19 +151,19 @@ public class ChartView extends View {
|
||||
// Check if the y event is within markerHeight of the marker center
|
||||
if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2f) < markerHeight) {
|
||||
int minDistance = Integer.MAX_VALUE;
|
||||
Marker nearestWaypoint = null;
|
||||
synchronized (waypoints) {
|
||||
for (Marker waypoint : waypoints) {
|
||||
int distance = Math.abs(getX(getWaypointXValue(waypoint)) - (int) event.getX() - getScrollX());
|
||||
Marker nearestMarker = null;
|
||||
synchronized (markers) {
|
||||
for (Marker marker : markers) {
|
||||
int distance = Math.abs(getX(getMarkerXValue(marker)) - (int) event.getX() - getScrollX());
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
nearestWaypoint = waypoint;
|
||||
nearestMarker = marker;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nearestWaypoint != null && minDistance < markerWidth) {
|
||||
if (nearestMarker != null && minDistance < markerWidth) {
|
||||
Intent intent = IntentUtils.newIntent(getContext(), MarkerDetailActivity.class)
|
||||
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, nearestWaypoint.getId());
|
||||
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, nearestMarker.getId());
|
||||
getContext().startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
@@ -191,7 +192,6 @@ public class ChartView extends View {
|
||||
public ChartView(Context context, final boolean chartByDistance) {
|
||||
super(context);
|
||||
this.chartByDistance = chartByDistance;
|
||||
|
||||
seriesList.add(new ChartValueSeries(context,
|
||||
Integer.MIN_VALUE,
|
||||
Integer.MAX_VALUE,
|
||||
@@ -307,11 +307,13 @@ public class ChartView extends View {
|
||||
}
|
||||
});
|
||||
|
||||
backgroundColor = ThemeUtils.getBackgroundColor(context);
|
||||
|
||||
float scale = context.getResources().getDisplayMetrics().density;
|
||||
|
||||
axisPaint = new Paint();
|
||||
axisPaint.setStyle(Style.STROKE);
|
||||
axisPaint.setColor(context.getResources().getColor(android.R.color.black));
|
||||
axisPaint.setStyle(Style.FILL_AND_STROKE);
|
||||
axisPaint.setColor(ThemeUtils.getTextColorPrimary(context));
|
||||
axisPaint.setAntiAlias(true);
|
||||
axisPaint.setTextSize(SMALL_TEXT_SIZE * scale);
|
||||
|
||||
@@ -320,22 +322,21 @@ public class ChartView extends View {
|
||||
|
||||
gridPaint = new Paint();
|
||||
gridPaint.setStyle(Style.STROKE);
|
||||
gridPaint.setColor(context.getResources().getColor(android.R.color.darker_gray));
|
||||
gridPaint.setColor(ThemeUtils.getTextColorSecondary(context));
|
||||
gridPaint.setAntiAlias(false);
|
||||
gridPaint.setPathEffect(new DashPathEffect(new float[]{3, 2}, 0));
|
||||
|
||||
markerPaint = new Paint();
|
||||
markerPaint.setStyle(Style.STROKE);
|
||||
markerPaint.setColor(context.getResources().getColor(android.R.color.darker_gray));
|
||||
markerPaint.setAntiAlias(false);
|
||||
|
||||
pointer = context.getResources().getDrawable(R.drawable.ic_logo_color_24dp);
|
||||
pointer.setBounds(0, 0, pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight());
|
||||
|
||||
waypointMarker = MarkerUtils.getDefaultPhoto(context);
|
||||
markerWidth = waypointMarker.getIntrinsicWidth();
|
||||
markerHeight = waypointMarker.getIntrinsicHeight();
|
||||
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
|
||||
markerPin = MarkerUtils.getDefaultPhoto(context);
|
||||
markerWidth = markerPin.getIntrinsicWidth();
|
||||
markerHeight = markerPin.getIntrinsicHeight();
|
||||
markerPin.setBounds(0, 0, markerWidth, markerHeight);
|
||||
|
||||
scroller = new Scroller(context);
|
||||
setFocusable(true);
|
||||
@@ -434,15 +435,15 @@ public class ChartView extends View {
|
||||
scrollTo(0, 0);
|
||||
}
|
||||
|
||||
public void addWaypoint(Marker waypoint) {
|
||||
synchronized (waypoints) {
|
||||
waypoints.add(waypoint);
|
||||
public void addMarker(Marker marker) {
|
||||
synchronized (markers) {
|
||||
markers.add(marker);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearWaypoints() {
|
||||
synchronized (waypoints) {
|
||||
waypoints.clear();
|
||||
public void clearMarker() {
|
||||
synchronized (markers) {
|
||||
markers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,13 +541,13 @@ public class ChartView extends View {
|
||||
synchronized (chartPoints) {
|
||||
canvas.save();
|
||||
|
||||
canvas.drawColor(Color.WHITE);
|
||||
canvas.drawColor(backgroundColor);
|
||||
|
||||
canvas.save();
|
||||
|
||||
clipToGraphArea(canvas);
|
||||
drawDataSeries(canvas);
|
||||
drawWaypoints(canvas);
|
||||
drawMarker(canvas);
|
||||
drawGrid(canvas);
|
||||
|
||||
canvas.restore();
|
||||
@@ -587,24 +588,19 @@ public class ChartView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the waypoints.
|
||||
*
|
||||
* @param canvas the canvas
|
||||
*/
|
||||
private void drawWaypoints(Canvas canvas) {
|
||||
synchronized (waypoints) {
|
||||
for (Marker waypoint : waypoints) {
|
||||
double xValue = getWaypointXValue(waypoint);
|
||||
private void drawMarker(Canvas canvas) {
|
||||
synchronized (markers) {
|
||||
for (Marker marker : markers) {
|
||||
double xValue = getMarkerXValue(marker);
|
||||
if (xValue > maxX) {
|
||||
continue;
|
||||
}
|
||||
canvas.save();
|
||||
float x = getX(getWaypointXValue(waypoint));
|
||||
float x = getX(getMarkerXValue(marker));
|
||||
canvas.drawLine(x, topBorder + spacer + markerHeight / 2, x, topBorder + effectiveHeight, markerPaint);
|
||||
canvas.translate(x - (markerWidth * WAYPOINT_X_ANCHOR), topBorder + spacer);
|
||||
canvas.translate(x - (markerWidth * MARKER_X_ANCHOR), topBorder + spacer);
|
||||
|
||||
waypointMarker.draw(canvas);
|
||||
markerPin.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
@@ -904,7 +900,7 @@ public class ChartView extends View {
|
||||
for (ChartValueSeries chartValueSeries : seriesList) {
|
||||
chartValueSeries.updateDimension();
|
||||
}
|
||||
float density = getContext().getResources().getDisplayMetrics().density;
|
||||
float density = getResources().getDisplayMetrics().density;
|
||||
spacer = (int) (density * SPACER);
|
||||
yAxisOffset = (int) (density * Y_AXIS_OFFSET);
|
||||
|
||||
@@ -975,17 +971,12 @@ public class ChartView extends View {
|
||||
return topBorder + yAxisOffset + (int) ((1 - percentage) * rangeHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a waypoint's x value.
|
||||
*
|
||||
* @param waypoint the waypoint
|
||||
*/
|
||||
private double getWaypointXValue(Marker waypoint) {
|
||||
private double getMarkerXValue(Marker marker) {
|
||||
if (chartByDistance) {
|
||||
double lenghtInKm = waypoint.getLength() * UnitConversions.M_TO_KM;
|
||||
return metricUnits ? lenghtInKm : lenghtInKm * UnitConversions.KM_TO_MI;
|
||||
double length_km = marker.getLength() * UnitConversions.M_TO_KM;
|
||||
return metricUnits ? length_km : length_km * UnitConversions.KM_TO_MI;
|
||||
} else {
|
||||
return waypoint.getDuration();
|
||||
return marker.getDuration();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,9 @@ public class DescriptionGenerator {
|
||||
}
|
||||
|
||||
// Elevation gain
|
||||
writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
|
||||
if (stats.hasTotalElevationGain()) {
|
||||
writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
|
||||
}
|
||||
|
||||
// Recorded time
|
||||
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime_ms())));
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package de.dennisguse.opentracks.content;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.MarkerColumns;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
/**
|
||||
* Engine for searching for tracks and markers by text.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class SearchEngine {
|
||||
|
||||
/**
|
||||
* WHERE query to get tracks by name.
|
||||
*/
|
||||
private static final String TRACK_SELECTION_QUERY =
|
||||
TracksColumns.NAME + " LIKE ? OR " +
|
||||
TracksColumns.DESCRIPTION + " LIKE ? OR " +
|
||||
TracksColumns.CATEGORY + " LIKE ?";
|
||||
|
||||
/**
|
||||
* WHERE query to get markers by name.
|
||||
*/
|
||||
private static final String MARKER_SELECTION_QUERY =
|
||||
MarkerColumns.NAME + " LIKE ? OR " +
|
||||
MarkerColumns.DESCRIPTION + " LIKE ? OR " +
|
||||
MarkerColumns.CATEGORY + " LIKE ?";
|
||||
|
||||
/**
|
||||
* Order of track results.
|
||||
*/
|
||||
private static final String TRACK_SELECTION_ORDER = TracksColumns._ID + " DESC LIMIT 1000";
|
||||
|
||||
/**
|
||||
* Order of marker results.
|
||||
*/
|
||||
private static final String MARKER_SELECTION_ORDER = MarkerColumns._ID + " DESC";
|
||||
|
||||
/**
|
||||
* How much we promote a match in the track category.
|
||||
*/
|
||||
private static final double TRACK_CATEGORY_PROMOTION = 2.0;
|
||||
|
||||
/**
|
||||
* How much we promote a match in the track description.
|
||||
*/
|
||||
private static final double TRACK_DESCRIPTION_PROMOTION = 8.0;
|
||||
|
||||
/**
|
||||
* How much we promote a match in the track name.
|
||||
*/
|
||||
private static final double TRACK_NAME_PROMOTION = 16.0;
|
||||
|
||||
/**
|
||||
* How much we promote a marker result if it's in the currently-selected track.
|
||||
*/
|
||||
private static final double CURRENT_TRACK_MARKER_PROMOTION = 2.0;
|
||||
|
||||
/**
|
||||
* How much we promote a track result if it's the currently-selected track.
|
||||
*/
|
||||
private static final double CURRENT_TRACK_DEMOTION = 0.5;
|
||||
|
||||
/**
|
||||
* Maximum number of markers which will be retrieved and scored.
|
||||
*/
|
||||
private static final int MAX_SCORED_MARKERS = 100;
|
||||
|
||||
/**
|
||||
* Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000)
|
||||
*/
|
||||
public static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L;
|
||||
|
||||
/**
|
||||
* Comparator for scored results.
|
||||
*/
|
||||
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR = (r1, r2) -> {
|
||||
// Score ordering.
|
||||
int scoreDiff = Double.compare(r2.score, r1.score);
|
||||
if (scoreDiff != 0) {
|
||||
return scoreDiff;
|
||||
}
|
||||
|
||||
// Make tracks come before markers.
|
||||
if (r1.marker != null && r2.track != null) {
|
||||
return 1;
|
||||
} else if (r1.track != null && r2.marker != null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Finally, use arbitrary ordering, by ID.
|
||||
long id1 = r1.track != null ? r1.track.getId().getId() : r1.marker.getId().getId();
|
||||
long id2 = r2.track != null ? r2.track.getId().getId() : r2.marker.getId().getId();
|
||||
long idDiff = id2 - id1;
|
||||
return Long.signum(idDiff);
|
||||
};
|
||||
|
||||
private final ContentProviderUtils providerUtils;
|
||||
|
||||
public SearchEngine(ContentProviderUtils providerUtils) {
|
||||
this.providerUtils = providerUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* Squashes a number by calculating 1 / log (1 + x).
|
||||
*/
|
||||
//TODO Why is this done?
|
||||
private static double squash(double x) {
|
||||
return 1.0 / Math.log1p(x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a searchable query and returns a set of sorted results.
|
||||
*
|
||||
* @param query the query to execute
|
||||
* @return a set of results, sorted according to their score
|
||||
*/
|
||||
public SortedSet<ScoredResult> search(SearchQuery query) {
|
||||
TreeSet<ScoredResult> scoredResults = new TreeSet<>(SCORED_RESULT_COMPARATOR);
|
||||
|
||||
scoreTrackResults(retrieveTracks(query), query, scoredResults);
|
||||
scoreMarkersResults(retrieveMarkers(query), query, scoredResults);
|
||||
|
||||
return scoredResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves tracks matching the given query from the database.
|
||||
*
|
||||
* @param query the query to retrieve for
|
||||
*/
|
||||
private List<Track> retrieveTracks(SearchQuery query) {
|
||||
ArrayList<Track> tracks = new ArrayList<>();
|
||||
|
||||
String queryLikeSelection = "%" + query.textQuery + "%";
|
||||
String[] trackSelectionArgs = new String[]{
|
||||
queryLikeSelection,
|
||||
queryLikeSelection,
|
||||
queryLikeSelection
|
||||
};
|
||||
|
||||
try (Cursor cursor = providerUtils.getTrackCursor(TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER)) {
|
||||
if (cursor != null) {
|
||||
tracks.ensureCapacity(cursor.getCount());
|
||||
while (cursor.moveToNext()) {
|
||||
tracks.add(providerUtils.createTrack(cursor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves markers matching the given query from the database.
|
||||
*
|
||||
* @param query the query to retrieve for
|
||||
*/
|
||||
private List<Marker> retrieveMarkers(SearchQuery query) {
|
||||
ArrayList<Marker> markers = new ArrayList<>();
|
||||
|
||||
String queryLikeSelection2 = "%" + query.textQuery + "%";
|
||||
String[] markersSelectionArgs = new String[]{
|
||||
queryLikeSelection2,
|
||||
queryLikeSelection2,
|
||||
queryLikeSelection2
|
||||
};
|
||||
|
||||
try (Cursor cursor = providerUtils.getMarkerCursor(MARKER_SELECTION_QUERY, markersSelectionArgs, MARKER_SELECTION_ORDER, MAX_SCORED_MARKERS)) {
|
||||
if (cursor != null) {
|
||||
markers.ensureCapacity(cursor.getCount());
|
||||
while (cursor.moveToNext()) {
|
||||
Marker marker = providerUtils.createMarker(cursor);
|
||||
if (LocationUtils.isValidLocation(marker.getLocation())) {
|
||||
markers.add(marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scores a collection of track results.
|
||||
*
|
||||
* @param tracks the results to score
|
||||
* @param query the query to score for
|
||||
* @param output the collection to fill with scored results
|
||||
*/
|
||||
private void scoreTrackResults(Collection<Track> tracks, SearchQuery query, Collection<ScoredResult> output) {
|
||||
for (Track track : tracks) {
|
||||
// Calculate the score.
|
||||
double score = scoreTrackResult(query, track);
|
||||
|
||||
// Add to the output.
|
||||
output.add(new ScoredResult(track, score));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scores a single track result.
|
||||
*
|
||||
* @param query the query to score for
|
||||
* @param track the results to score
|
||||
* @return the score for the track
|
||||
*/
|
||||
private double scoreTrackResult(SearchQuery query, Track track) {
|
||||
double score = 1.0;
|
||||
|
||||
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
|
||||
|
||||
TrackStatistics statistics = track.getTrackStatistics();
|
||||
|
||||
long meanTimestamp = (statistics.getStartTime_ms() + statistics.getStopTime_ms()) / 2L;
|
||||
score *= getTimeBoost(query, meanTimestamp);
|
||||
|
||||
// Score the currently-selected track lower (user is already there, wouldn't be searching for it).
|
||||
if (track.getId() == query.currentTrackId) {
|
||||
score *= CURRENT_TRACK_DEMOTION;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scores a collection of marker results.
|
||||
*
|
||||
* @param markers the results to score
|
||||
* @param query the query to score for
|
||||
* @param output the collection to fill with scored results
|
||||
*/
|
||||
private void scoreMarkersResults(Collection<Marker> markers, SearchQuery query, Collection<ScoredResult> output) {
|
||||
for (Marker marker : markers) {
|
||||
// Calculate the score.
|
||||
double score = scoreMarkerResult(query, marker);
|
||||
|
||||
// Add to the output.
|
||||
output.add(new ScoredResult(marker, score));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scores a single marker result.
|
||||
*
|
||||
* @param query the query to score for
|
||||
* @param marker the results to score
|
||||
* @return the score for the marker
|
||||
*/
|
||||
private double scoreMarkerResult(SearchQuery query, Marker marker) {
|
||||
double score = 1.0;
|
||||
|
||||
Location location = marker.getLocation();
|
||||
score *= getTitleBoost(query, marker.getName(), marker.getDescription(), marker.getCategory());
|
||||
score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude());
|
||||
score *= getTimeBoost(query, location.getTime());
|
||||
|
||||
// Score markers in the currently-selected track higher (searching inside the current track).
|
||||
if (marker.getTrackId() != null && marker.getTrackId().equals(query.currentTrackId)) {
|
||||
score *= CURRENT_TRACK_MARKER_PROMOTION;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the boosting of the score due to the field(s) in which the match occured.
|
||||
*
|
||||
* @param query the query to boost for
|
||||
* @param name the name of the track or marker
|
||||
* @param description the description of the track or marker
|
||||
* @param category the category of the track or marker
|
||||
* @return the total boost to be applied to the result
|
||||
*/
|
||||
private double getTitleBoost(SearchQuery query, String name, String description, String category) {
|
||||
// Title boost: track name > description > category.
|
||||
double boost = 1.0;
|
||||
if (name.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
|
||||
boost *= TRACK_NAME_PROMOTION;
|
||||
}
|
||||
if (description.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
|
||||
boost *= TRACK_DESCRIPTION_PROMOTION;
|
||||
}
|
||||
if (category.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
|
||||
boost *= TRACK_CATEGORY_PROMOTION;
|
||||
}
|
||||
return boost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the boosting of the score due to the recency of the matched entity.
|
||||
*
|
||||
* @param query the query to boost for
|
||||
* @param timestamp the timestamp to calculate the boost for
|
||||
* @return the total boost to be applied to the result
|
||||
*/
|
||||
private double getTimeBoost(SearchQuery query, long timestamp) {
|
||||
if (timestamp < OLDEST_ALLOWED_TIMESTAMP) {
|
||||
// Safety: if timestamp is too old or invalid, don't rank based on time.
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Score recent tracks higher.
|
||||
long timeAgoHours = (long) ((query.currentTimestamp - timestamp) * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN * UnitConversions.MIN_TO_HR);
|
||||
if (timeAgoHours > 0L) {
|
||||
return squash(timeAgoHours);
|
||||
} else {
|
||||
// Should rarely happen (track recorded in the last hour).
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the boosting of the score due to proximity to a location.
|
||||
*
|
||||
* @param query the query to boost for
|
||||
* @param latitude the latitude to calculate the boost for
|
||||
* @param longitude the longitude to calculate the boost for
|
||||
* @return the total boost to be applied to the result
|
||||
*/
|
||||
private double getDistanceBoost(SearchQuery query, double latitude, double longitude) {
|
||||
if (query.currentLocation == null) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
float[] distanceResults = new float[1];
|
||||
|
||||
Location.distanceBetween(
|
||||
latitude, longitude,
|
||||
query.currentLocation.getLatitude(), query.currentLocation.getLongitude(),
|
||||
distanceResults);
|
||||
|
||||
// Score tracks close to the current location higher.
|
||||
double distanceKm = distanceResults[0] * UnitConversions.M_TO_KM;
|
||||
|
||||
if (distanceKm > 0.0) {
|
||||
// Use the inverse of the amortized distance.
|
||||
return squash(distanceKm);
|
||||
} else {
|
||||
// Should rarely happen (distance is exactly 0).
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of a searchable query, along with all contextual data needed to execute it.
|
||||
*/
|
||||
public static class SearchQuery {
|
||||
final String textQuery;
|
||||
final Location currentLocation;
|
||||
final Track.Id currentTrackId;
|
||||
final long currentTimestamp;
|
||||
|
||||
public SearchQuery(String textQuery, Location currentLocation, Track.Id currentTrackId, long currentTimestamp) {
|
||||
this.textQuery = textQuery.toLowerCase(Locale.getDefault());
|
||||
this.currentLocation = currentLocation;
|
||||
this.currentTrackId = currentTrackId;
|
||||
this.currentTimestamp = currentTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of a searchable result which has been retrieved and scored.
|
||||
*/
|
||||
public static class ScoredResult {
|
||||
public final Track track;
|
||||
public final Marker marker;
|
||||
final double score;
|
||||
|
||||
ScoredResult(Track track, double score) {
|
||||
this.track = track;
|
||||
this.marker = null;
|
||||
this.score = score;
|
||||
}
|
||||
|
||||
ScoredResult(Marker marker, double score) {
|
||||
this.track = null;
|
||||
this.marker = marker;
|
||||
this.score = score;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ScoredResult ["
|
||||
+ (track != null ? ("trackId=" + track.getId().getId() + ", ") : "")
|
||||
+ (marker != null ? ("wptId=" + marker.getId().getId() + ", ") : "")
|
||||
+ "score=" + score + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,17 +54,15 @@ import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
public class TrackDataHub implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
/**
|
||||
* Target number of track points displayed by the map overlay.
|
||||
* Target number of track points displayed by the diagrams (recommended).
|
||||
* We may display more than this number of points.
|
||||
*/
|
||||
@Deprecated
|
||||
private static final int TARGET_DISPLAYED_TRACKPOINTS = 5000;
|
||||
|
||||
/**
|
||||
* Maximum number of markers to displayed.
|
||||
* Maximum number of markers to displayed in the diagrams.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
@Deprecated
|
||||
private static final int MAX_DISPLAYED_MARKERS = 128;
|
||||
|
||||
private static final String TAG = TrackDataHub.class.getSimpleName();
|
||||
|
||||
@@ -40,20 +40,28 @@ public final class Marker {
|
||||
private String description = "";
|
||||
private String category = "";
|
||||
private String icon = "";
|
||||
private Track.Id trackId;
|
||||
private final Track.Id trackId;
|
||||
//TODO It is the distance from the track starting point; rename to something more meaningful
|
||||
private double length = 0.0;
|
||||
private long duration = 0;
|
||||
private Location location;
|
||||
private final Location location;
|
||||
@Deprecated //TODO Make an URI instead of String
|
||||
private String photoUrl = "";
|
||||
|
||||
@VisibleForTesting
|
||||
public Marker(@NonNull TrackPoint trackPoint) {
|
||||
this.location = trackPoint.getLocation();
|
||||
public Marker(@NonNull Track.Id trackId, @NonNull TrackPoint trackPoint) {
|
||||
this(trackId, trackPoint.getLocation());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
//TODO Used by AbstractFileImporter to create an intermediate marker before saving a new one into the database.
|
||||
public Marker(@NonNull Location location) {
|
||||
this(null, location);
|
||||
}
|
||||
|
||||
public Marker(@NonNull Track.Id trackId, @NonNull Location location) {
|
||||
this.location = location;
|
||||
this.trackId = trackId;
|
||||
}
|
||||
|
||||
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, double length, long duration, @NonNull Location location, String photoUrl) {
|
||||
@@ -69,7 +77,7 @@ public final class Marker {
|
||||
}
|
||||
|
||||
/**
|
||||
* May be null if the waypoint was not loaded from the database.
|
||||
* May be null if the it was not loaded from the database.
|
||||
*/
|
||||
public @Nullable
|
||||
Id getId() {
|
||||
@@ -112,14 +120,11 @@ public final class Marker {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
public Track.Id getTrackId() {
|
||||
public @NonNull
|
||||
Track.Id getTrackId() {
|
||||
return trackId;
|
||||
}
|
||||
|
||||
public void setTrackId(Track.Id trackId) {
|
||||
this.trackId = trackId;
|
||||
}
|
||||
|
||||
public double getLength() {
|
||||
return length;
|
||||
}
|
||||
@@ -165,16 +170,12 @@ public final class Marker {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Id(@NonNull String id) {
|
||||
this(Long.parseLong(id));
|
||||
}
|
||||
|
||||
//TOOD Limit visibility to TrackRecordingService / ContentProvider
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Deprecated //TODO Use a Waypoint.Id of null instead
|
||||
@Deprecated //TODO Use a Id of null instead
|
||||
public boolean isValid() {
|
||||
return id != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
}
|
||||
|
||||
@@ -22,13 +22,13 @@ import android.provider.BaseColumns;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
|
||||
/**
|
||||
* Constants for waypoints table.
|
||||
* Constants for markers table.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public interface MarkerColumns extends BaseColumns {
|
||||
|
||||
String TABLE_NAME = "waypoints";
|
||||
String TABLE_NAME = "markers";
|
||||
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
|
||||
Uri CONTENT_URI_BY_TRACKID = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/trackid");
|
||||
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint";
|
||||
@@ -36,10 +36,10 @@ public interface MarkerColumns extends BaseColumns {
|
||||
String DEFAULT_SORT_ORDER = _ID;
|
||||
|
||||
// Columns
|
||||
String NAME = "name"; // waypoint name
|
||||
String DESCRIPTION = "description"; // waypoint description
|
||||
String CATEGORY = "category"; // waypoint category
|
||||
String ICON = "icon"; // waypoint icon
|
||||
String NAME = "name"; // marker name
|
||||
String DESCRIPTION = "description"; // marker description
|
||||
String CATEGORY = "category"; // marker category
|
||||
String ICON = "icon"; // marker icon
|
||||
String TRACKID = "trackid"; // track id
|
||||
|
||||
String LENGTH = "length"; // length of the track (without smoothing)
|
||||
@@ -60,7 +60,7 @@ public interface MarkerColumns extends BaseColumns {
|
||||
+ DESCRIPTION + " TEXT, "
|
||||
+ CATEGORY + " TEXT, "
|
||||
+ ICON + " TEXT, "
|
||||
+ TRACKID + " INTEGER, "
|
||||
+ TRACKID + " INTEGER NOT NULL, "
|
||||
+ LENGTH + " FLOAT, "
|
||||
+ DURATION + " INTEGER, "
|
||||
+ LONGITUDE + " INTEGER, "
|
||||
@@ -69,7 +69,8 @@ public interface MarkerColumns extends BaseColumns {
|
||||
+ ALTITUDE + " FLOAT, "
|
||||
+ ACCURACY + " FLOAT, "
|
||||
+ BEARING + " FLOAT, "
|
||||
+ PHOTOURL + " TEXT"
|
||||
+ PHOTOURL + " TEXT, "
|
||||
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
String CREATE_TABLE_INDEX = "CREATE INDEX " + TABLE_NAME + "_" + TRACKID + "_index ON " + TABLE_NAME + "(" + TRACKID + ")";
|
||||
|
||||
@@ -118,8 +118,8 @@ public class Track {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Id(@NonNull String id) {
|
||||
this(Long.parseLong(id));
|
||||
protected Id(Parcel in) {
|
||||
id = in.readLong();
|
||||
}
|
||||
|
||||
//TOOD Limit visibility to TrackRecordingService / ContentProvider
|
||||
@@ -153,7 +153,7 @@ public class Track {
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
throw new RuntimeException("Not supported");
|
||||
return String.valueOf(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -32,6 +32,7 @@ public class TrackPoint {
|
||||
private Float heartRate_bpm = null;
|
||||
private Float cyclingCadence_rpm = null;
|
||||
private Float power = null;
|
||||
private Float elevationGain = null;
|
||||
|
||||
public TrackPoint() {
|
||||
this.location = new Location("");
|
||||
@@ -47,6 +48,8 @@ public class TrackPoint {
|
||||
this.heartRate_bpm = trackPoint.getHeartRate_bpm();
|
||||
this.cyclingCadence_rpm = trackPoint.getCyclingCadence_rpm();
|
||||
this.power = trackPoint.getPower();
|
||||
|
||||
this.elevationGain = trackPoint.getElevationGain();
|
||||
}
|
||||
|
||||
public TrackPoint(double latitude, double longitude, Double altitude, long time) {
|
||||
@@ -92,6 +95,18 @@ public class TrackPoint {
|
||||
return location;
|
||||
}
|
||||
|
||||
public boolean hasElevationGain() {
|
||||
return elevationGain != null;
|
||||
}
|
||||
|
||||
public float getElevationGain() {
|
||||
return elevationGain;
|
||||
}
|
||||
|
||||
public void setElevationGain(Float elevationGain) {
|
||||
this.elevationGain = elevationGain;
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return location.getLatitude();
|
||||
}
|
||||
|
||||
@@ -40,7 +40,9 @@ public interface TrackPointsColumns extends BaseColumns {
|
||||
|
||||
String LONGITUDE = "longitude";
|
||||
String LATITUDE = "latitude";
|
||||
@Deprecated
|
||||
double PAUSE_LATITUDE = 100.0;
|
||||
@Deprecated
|
||||
double RESUME_LATITUDE = 200.0;
|
||||
|
||||
String TIME = "time";
|
||||
@@ -51,10 +53,11 @@ public interface TrackPointsColumns extends BaseColumns {
|
||||
String SENSOR_HEARTRATE = "sensor_heartrate";
|
||||
String SENSOR_CADENCE = "sensor_cadence";
|
||||
String SENSOR_POWER = "sensor_power";
|
||||
String ELEVATION_GAIN = "elevation_gain";
|
||||
|
||||
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ TRACKID + " INTEGER, "
|
||||
+ TRACKID + " INTEGER NOT NULL, "
|
||||
+ LONGITUDE + " INTEGER, "
|
||||
+ LATITUDE + " INTEGER, "
|
||||
+ TIME + " INTEGER, "
|
||||
@@ -64,7 +67,10 @@ public interface TrackPointsColumns extends BaseColumns {
|
||||
+ BEARING + " FLOAT, "
|
||||
+ SENSOR_HEARTRATE + " FLOAT, "
|
||||
+ SENSOR_CADENCE + " FLOAT, "
|
||||
+ SENSOR_POWER + " FLOAT)";
|
||||
+ SENSOR_POWER + " FLOAT, "
|
||||
+ ELEVATION_GAIN + " FLOAT, "
|
||||
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
String CREATE_TABLE_INDEX = "CREATE INDEX " + TABLE_NAME + "_" + TRACKID + "_index ON " + TABLE_NAME + "(" + TRACKID + ")";
|
||||
}
|
||||
|
||||
@@ -35,12 +35,13 @@ public interface TracksColumns extends BaseColumns {
|
||||
String DEFAULT_SORT_ORDER = _ID;
|
||||
|
||||
// Columns
|
||||
String UUID = "uuid";
|
||||
String UUID = "uuid"; // identifier to make tracks globally unique (prevent re-import)
|
||||
String NAME = "name"; // track name
|
||||
String DESCRIPTION = "description"; // track description
|
||||
String CATEGORY = "category"; // track activity type
|
||||
String STARTTIME = "starttime"; // track start time
|
||||
String STOPTIME = "stoptime"; // track stop time
|
||||
String MARKER_COUNT = "markerCount"; // the numbers of markers (virtual column)
|
||||
@Deprecated
|
||||
String NUMPOINTS = "numpoints"; // number of track points //TODO UNUSED
|
||||
String TOTALDISTANCE = "totaldistance"; // total distance
|
||||
@@ -55,25 +56,25 @@ public interface TracksColumns extends BaseColumns {
|
||||
String ELEVATIONGAIN = "elevationgain"; // elevation gain
|
||||
String ICON = "icon"; // track activity type icon
|
||||
|
||||
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" // table
|
||||
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " // id
|
||||
+ NAME + " TEXT, " // name
|
||||
+ DESCRIPTION + " TEXT, " // description
|
||||
+ CATEGORY + " TEXT, " // category
|
||||
+ STARTTIME + " INTEGER, " // start time
|
||||
+ STOPTIME + " INTEGER, " // stop time
|
||||
+ NUMPOINTS + " INTEGER, " // num points
|
||||
+ TOTALDISTANCE + " FLOAT, " // total distance
|
||||
+ TOTALTIME + " INTEGER, " // total time
|
||||
+ MOVINGTIME + " INTEGER, " // moving time
|
||||
+ AVGSPEED + " FLOAT, " // average speed
|
||||
+ AVGMOVINGSPEED + " FLOAT, " // average moving speed
|
||||
+ MAXSPEED + " FLOAT, " // max speed
|
||||
+ MINELEVATION + " FLOAT, " // min elevation
|
||||
+ MAXELEVATION + " FLOAT, " // max elevation
|
||||
+ ELEVATIONGAIN + " FLOAT, " // elevation gain
|
||||
+ ICON + " TEXT, " // icon
|
||||
+ UUID + " BLOB)"; // UUID
|
||||
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ NAME + " TEXT, "
|
||||
+ DESCRIPTION + " TEXT, "
|
||||
+ CATEGORY + " TEXT, "
|
||||
+ STARTTIME + " INTEGER, "
|
||||
+ STOPTIME + " INTEGER, "
|
||||
+ NUMPOINTS + " INTEGER, "
|
||||
+ TOTALDISTANCE + " FLOAT, "
|
||||
+ TOTALTIME + " INTEGER, "
|
||||
+ MOVINGTIME + " INTEGER, "
|
||||
+ AVGSPEED + " FLOAT, "
|
||||
+ AVGMOVINGSPEED + " FLOAT, "
|
||||
+ MAXSPEED + " FLOAT, "
|
||||
+ MINELEVATION + " FLOAT, "
|
||||
+ MAXELEVATION + " FLOAT, "
|
||||
+ ELEVATIONGAIN + " FLOAT, "
|
||||
+ ICON + " TEXT, "
|
||||
+ UUID + " BLOB)";
|
||||
|
||||
String CREATE_TABLE_INDEX = "CREATE UNIQUE INDEX " + TABLE_NAME + "_" + UUID + "_index ON " + TABLE_NAME + "(" + UUID + ")";
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ class CachedTrackPointsIndexes {
|
||||
final int sensorHeartRateIndex;
|
||||
final int sensorCadenceIndex;
|
||||
final int sensorPowerIndex;
|
||||
final int elevationGainIndex;
|
||||
|
||||
CachedTrackPointsIndexes(Cursor cursor) {
|
||||
idIndex = cursor.getColumnIndex(TrackPointsColumns._ID);
|
||||
@@ -32,5 +33,6 @@ class CachedTrackPointsIndexes {
|
||||
sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE);
|
||||
sensorCadenceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_CADENCE);
|
||||
sensorPowerIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_POWER);
|
||||
elevationGainIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ELEVATION_GAIN);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,18 +28,15 @@ import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.loader.content.CursorLoader;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.BuildConfig;
|
||||
import de.dennisguse.opentracks.android.ContentResolverWrapper;
|
||||
import de.dennisguse.opentracks.android.IContentResolver;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.MarkerColumns;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
@@ -48,12 +45,10 @@ import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.TrackUtils;
|
||||
import de.dennisguse.opentracks.util.UUIDUtils;
|
||||
|
||||
/**
|
||||
* {@link ContentProviderUtils} implementation.
|
||||
* Allows to use {@link ContentResolver} and {@link android.content.ContentProvider} interchangeably via {@link IContentResolver}.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
@@ -73,18 +68,15 @@ public class ContentProviderUtils {
|
||||
public static final int MAX_LOADED_MARKERS = 10000;
|
||||
private static final String ID_SEPARATOR = ",";
|
||||
|
||||
private final IContentResolver contentResolver;
|
||||
private final ContentResolver contentResolver;
|
||||
private int defaultCursorBatchSize = 2000;
|
||||
|
||||
public ContentProviderUtils(Context context) {
|
||||
this(context.getContentResolver());
|
||||
contentResolver = context.getContentResolver();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public ContentProviderUtils(ContentResolver contentResolver) {
|
||||
this.contentResolver = new ContentResolverWrapper(contentResolver);
|
||||
}
|
||||
|
||||
public ContentProviderUtils(IContentResolver contentResolver) {
|
||||
this.contentResolver = contentResolver;
|
||||
}
|
||||
|
||||
@@ -94,6 +86,7 @@ public class ContentProviderUtils {
|
||||
*
|
||||
* @param trackId the track id
|
||||
*/
|
||||
@Deprecated //TODO Figure out why we need this.
|
||||
public void clearTrack(Track.Id trackId) {
|
||||
deleteTrackPointsAndMarkers(trackId);
|
||||
Track track = new Track();
|
||||
@@ -106,7 +99,7 @@ public class ContentProviderUtils {
|
||||
*
|
||||
* @param cursor the cursor pointing to the track
|
||||
*/
|
||||
public Track createTrack(Cursor cursor) {
|
||||
public static Track createTrack(Cursor cursor) {
|
||||
int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID);
|
||||
int uuidIndex = cursor.getColumnIndexOrThrow(TracksColumns.UUID);
|
||||
int nameIndex = cursor.getColumnIndexOrThrow(TracksColumns.NAME);
|
||||
@@ -184,17 +177,18 @@ public class ContentProviderUtils {
|
||||
FileUtils.deleteDirectoryRecurse(dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a track.
|
||||
*
|
||||
* @param trackId the track id
|
||||
*/
|
||||
public void deleteTrack(Context context, Track.Id trackId) {
|
||||
if (trackId == null) {
|
||||
return;
|
||||
public void deleteTracks(Context context, @NonNull List<Track.Id> trackIds) {
|
||||
// Delete track folder resources.
|
||||
for (Track.Id trackId : trackIds) {
|
||||
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
|
||||
}
|
||||
deleteTrackPointsAndMarkers(trackId);
|
||||
|
||||
// Delete track last since it triggers a database vacuum call
|
||||
String whereClause = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
|
||||
contentResolver.delete(TracksColumns.CONTENT_URI, whereClause, trackIds.stream().map(id->Long.toString(id.getId())).toArray(String[]::new));
|
||||
}
|
||||
|
||||
public void deleteTrack(Context context, @NonNull Track.Id trackId) {
|
||||
// Delete track folder resources.
|
||||
FileUtils.deleteDirectoryRecurse(FileUtils.getPhotoDir(context, trackId));
|
||||
|
||||
@@ -223,14 +217,6 @@ public class ContentProviderUtils {
|
||||
return tracks;
|
||||
}
|
||||
|
||||
public static Loader<Cursor> getTracksCursorLoader(Context context) {
|
||||
String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME,
|
||||
TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME,
|
||||
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON};
|
||||
|
||||
return new CursorLoader(context, TracksColumns.CONTENT_URI, PROJECTION, null, null, TrackUtils.TRACK_SORT_ORDER);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
//TODO Do not use; rather get the proper TrackId (e.g., resume a track might give you some weird effects).
|
||||
@VisibleForTesting
|
||||
@@ -368,7 +354,8 @@ public class ContentProviderUtils {
|
||||
location.setBearing(cursor.getFloat(bearingIndex));
|
||||
}
|
||||
|
||||
Marker marker = new Marker(location);
|
||||
Track.Id trackId = new Track.Id(cursor.getLong(trackIdIndex));
|
||||
Marker marker = new Marker(trackId, location);
|
||||
|
||||
if (!cursor.isNull(idIndex)) {
|
||||
marker.setId(new Marker.Id(cursor.getLong(idIndex)));
|
||||
@@ -385,9 +372,6 @@ public class ContentProviderUtils {
|
||||
if (!cursor.isNull(iconIndex)) {
|
||||
marker.setIcon(cursor.getString(iconIndex));
|
||||
}
|
||||
if (!cursor.isNull(trackIdIndex)) {
|
||||
marker.setTrackId(new Track.Id(cursor.getLong(trackIdIndex)));
|
||||
}
|
||||
if (!cursor.isNull(lengthIndex)) {
|
||||
marker.setLength(cursor.getFloat(lengthIndex));
|
||||
}
|
||||
@@ -429,8 +413,7 @@ public class ContentProviderUtils {
|
||||
if (!markerId.isValid()) {
|
||||
return null;
|
||||
}
|
||||
try (Cursor cursor = getMarkerCursor(null, MarkerColumns._ID + "=?",
|
||||
new String[]{Long.toString(markerId.getId())}, MarkerColumns._ID, 1)) {
|
||||
try (Cursor cursor = getMarkerCursor(null, MarkerColumns._ID + "=?", new String[]{Long.toString(markerId.getId())}, MarkerColumns._ID, 1)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return createMarker(cursor);
|
||||
}
|
||||
@@ -438,18 +421,6 @@ public class ContentProviderUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The caller owns the returned cursor and is responsible for closing it.
|
||||
*
|
||||
* @param selection the selection. Can be null
|
||||
* @param selectionArgs the selection arguments. Can be null
|
||||
* @param sortOrder the sort order. Can be null
|
||||
* @param maxCount the maximum number of markers to return. -1 for no limit
|
||||
*/
|
||||
public Cursor getMarkerCursor(String selection, String[] selectionArgs, String sortOrder, int maxCount) {
|
||||
return getMarkerCursor(null, selection, selectionArgs, sortOrder, maxCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* The caller owns the returned cursor and is responsible for closing it.
|
||||
*
|
||||
@@ -488,22 +459,7 @@ public class ContentProviderUtils {
|
||||
return markers;
|
||||
}
|
||||
|
||||
public static Loader<Cursor> getMarkersLoader(Context context, @Nullable Track.Id trackId) {
|
||||
final String[] PROJECTION = new String[]{MarkerColumns._ID,
|
||||
MarkerColumns.NAME, MarkerColumns.DESCRIPTION, MarkerColumns.CATEGORY,
|
||||
MarkerColumns.TIME, MarkerColumns.PHOTOURL,
|
||||
MarkerColumns.LATITUDE, MarkerColumns.LONGITUDE};
|
||||
|
||||
if (trackId != null) {
|
||||
return new CursorLoader(context, MarkerColumns.CONTENT_URI, PROJECTION,
|
||||
MarkerColumns.TRACKID + "=?",
|
||||
new String[]{String.valueOf(trackId.getId())}, null);
|
||||
} else {
|
||||
return new CursorLoader(context, MarkerColumns.CONTENT_URI, PROJECTION, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated //TODO Expose information via Cursor
|
||||
@Deprecated //TODO TracksColumns.MARKER_COUNT while querying for tracks
|
||||
public int getMarkerCount(Track.Id trackId) {
|
||||
if (!trackId.isValid()) {
|
||||
return 0;
|
||||
@@ -566,9 +522,7 @@ public class ContentProviderUtils {
|
||||
values.put(MarkerColumns.DESCRIPTION, marker.getDescription());
|
||||
values.put(MarkerColumns.CATEGORY, marker.getCategory());
|
||||
values.put(MarkerColumns.ICON, marker.getIcon());
|
||||
if (marker.getTrackId() != null) {
|
||||
values.put(MarkerColumns.TRACKID, marker.getTrackId().getId());
|
||||
}
|
||||
values.put(MarkerColumns.TRACKID, marker.getTrackId().getId());
|
||||
values.put(MarkerColumns.LENGTH, marker.getLength());
|
||||
values.put(MarkerColumns.DURATION, marker.getDuration());
|
||||
|
||||
@@ -648,6 +602,10 @@ public class ContentProviderUtils {
|
||||
trackPoint.setPower(cursor.getFloat(indexes.sensorPowerIndex));
|
||||
}
|
||||
|
||||
if (!cursor.isNull(indexes.elevationGainIndex)) {
|
||||
trackPoint.setElevationGain(cursor.getFloat(indexes.elevationGainIndex));
|
||||
}
|
||||
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
@@ -836,6 +794,11 @@ public class ContentProviderUtils {
|
||||
if (trackPoint.hasPower()) {
|
||||
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getPower());
|
||||
}
|
||||
|
||||
if (trackPoint.hasElevationGain()) {
|
||||
values.put(TrackPointsColumns.ELEVATION_GAIN, trackPoint.getElevationGain());
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,12 +33,16 @@ import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.MarkerColumns;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
|
||||
/**
|
||||
* A {@link ContentProvider} that handles access to track points, tracks, and waypoints tables.
|
||||
* A {@link ContentProvider} that handles access to track points, tracks, and markers tables.
|
||||
* <p>
|
||||
* Data consistency is enforced using Foreign Key Constraints within the database incl. cascading deletes.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
@@ -61,9 +65,9 @@ public class CustomContentProvider extends ContentProvider {
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath(), UrlType.TRACKS.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath() + "/*", UrlType.TRACKS_BY_ID.ordinal());
|
||||
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.WAYPOINTS.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath() + "/#", UrlType.WAYPOINTS_BY_ID.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.WAYPOINTS_BY_TRACKID.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.MARKERS.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath() + "/#", UrlType.MARKERS_BY_ID.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.MARKERS_BY_TRACKID.ordinal());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,6 +86,8 @@ public class CustomContentProvider extends ContentProvider {
|
||||
CustomSQLiteOpenHelper databaseHelper = new CustomSQLiteOpenHelper(context);
|
||||
try {
|
||||
db = databaseHelper.getWritableDatabase();
|
||||
// Necessary to enable cascade deletion from Track to TrackPoints and Markers
|
||||
db.setForeignKeyConstraintsEnabled(true);
|
||||
} catch (SQLiteException e) {
|
||||
Log.e(TAG, "Unable to open database for writing.", e);
|
||||
}
|
||||
@@ -100,7 +106,7 @@ public class CustomContentProvider extends ContentProvider {
|
||||
table = TracksColumns.TABLE_NAME;
|
||||
shouldVacuum = true;
|
||||
break;
|
||||
case WAYPOINTS:
|
||||
case MARKERS:
|
||||
table = MarkerColumns.TABLE_NAME;
|
||||
break;
|
||||
default:
|
||||
@@ -138,10 +144,10 @@ public class CustomContentProvider extends ContentProvider {
|
||||
return TracksColumns.CONTENT_TYPE;
|
||||
case TRACKS_BY_ID:
|
||||
return TracksColumns.CONTENT_ITEMTYPE;
|
||||
case WAYPOINTS:
|
||||
case MARKERS:
|
||||
return MarkerColumns.CONTENT_TYPE;
|
||||
case WAYPOINTS_BY_ID:
|
||||
case WAYPOINTS_BY_TRACKID:
|
||||
case MARKERS_BY_ID:
|
||||
case MARKERS_BY_TRACKID:
|
||||
return MarkerColumns.CONTENT_ITEMTYPE;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URL " + url);
|
||||
@@ -206,22 +212,26 @@ public class CustomContentProvider extends ContentProvider {
|
||||
queryBuilder.appendWhere(TrackPointsColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
|
||||
break;
|
||||
case TRACKS:
|
||||
queryBuilder.setTables(TracksColumns.TABLE_NAME);
|
||||
if (projection != null && Arrays.asList(projection).contains(TracksColumns.MARKER_COUNT)) {
|
||||
queryBuilder.setTables(TracksColumns.TABLE_NAME + " LEFT OUTER JOIN (SELECT " + MarkerColumns.TRACKID + " AS markerTrackId, COUNT(*) AS " + TracksColumns.MARKER_COUNT + " FROM " + MarkerColumns.TABLE_NAME + " GROUP BY " + MarkerColumns.TRACKID + ") ON (" + TracksColumns.TABLE_NAME + "." + TracksColumns._ID + "= markerTrackId)");
|
||||
} else {
|
||||
queryBuilder.setTables(TracksColumns.TABLE_NAME);
|
||||
}
|
||||
sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER;
|
||||
break;
|
||||
case TRACKS_BY_ID:
|
||||
queryBuilder.setTables(TracksColumns.TABLE_NAME);
|
||||
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
|
||||
break;
|
||||
case WAYPOINTS:
|
||||
case MARKERS:
|
||||
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
|
||||
sortOrder = sort != null ? sort : MarkerColumns.DEFAULT_SORT_ORDER;
|
||||
break;
|
||||
case WAYPOINTS_BY_ID:
|
||||
case MARKERS_BY_ID:
|
||||
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
|
||||
queryBuilder.appendWhere(MarkerColumns._ID + "=" + ContentUris.parseId(url));
|
||||
break;
|
||||
case WAYPOINTS_BY_TRACKID:
|
||||
case MARKERS_BY_TRACKID:
|
||||
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
|
||||
queryBuilder.appendWhere(MarkerColumns.TRACKID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
|
||||
break;
|
||||
@@ -261,11 +271,11 @@ public class CustomContentProvider extends ContentProvider {
|
||||
whereClause += " AND (" + where + ")";
|
||||
}
|
||||
break;
|
||||
case WAYPOINTS:
|
||||
case MARKERS:
|
||||
table = MarkerColumns.TABLE_NAME;
|
||||
whereClause = where;
|
||||
break;
|
||||
case WAYPOINTS_BY_ID:
|
||||
case MARKERS_BY_ID:
|
||||
table = MarkerColumns.TABLE_NAME;
|
||||
whereClause = MarkerColumns._ID + "=" + ContentUris.parseId(url);
|
||||
if (!TextUtils.isEmpty(where)) {
|
||||
@@ -287,11 +297,6 @@ public class CustomContentProvider extends ContentProvider {
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link UrlType} for a url.
|
||||
*
|
||||
* @param url the url
|
||||
*/
|
||||
@NonNull
|
||||
private UrlType getUrlType(Uri url) {
|
||||
UrlType[] urlTypes = UrlType.values();
|
||||
@@ -316,19 +321,13 @@ public class CustomContentProvider extends ContentProvider {
|
||||
return insertTrackPoint(url, contentValues);
|
||||
case TRACKS:
|
||||
return insertTrack(url, contentValues);
|
||||
case WAYPOINTS:
|
||||
return insertWaypoint(url, contentValues);
|
||||
case MARKERS:
|
||||
return insertMarker(url, contentValues);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown url " + url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a track point.
|
||||
*
|
||||
* @param url the content url
|
||||
* @param values the content values
|
||||
*/
|
||||
private Uri insertTrackPoint(Uri url, ContentValues values) {
|
||||
boolean hasLatitude = values.containsKey(TrackPointsColumns.LATITUDE);
|
||||
boolean hasLongitude = values.containsKey(TrackPointsColumns.LONGITUDE);
|
||||
@@ -343,12 +342,6 @@ public class CustomContentProvider extends ContentProvider {
|
||||
throw new SQLiteException("Failed to insert a track point " + url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a track.
|
||||
*
|
||||
* @param url the content url
|
||||
* @param contentValues the content values
|
||||
*/
|
||||
private Uri insertTrack(Uri url, ContentValues contentValues) {
|
||||
boolean hasStartTime = contentValues.containsKey(TracksColumns.STARTTIME);
|
||||
if (!hasStartTime) {
|
||||
@@ -361,25 +354,14 @@ public class CustomContentProvider extends ContentProvider {
|
||||
throw new SQLException("Failed to insert a track " + url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a waypoint.
|
||||
*
|
||||
* @param url the content url
|
||||
* @param contentValues the content values
|
||||
*/
|
||||
private Uri insertWaypoint(Uri url, ContentValues contentValues) {
|
||||
private Uri insertMarker(Uri url, ContentValues contentValues) {
|
||||
long rowId = db.insert(MarkerColumns.TABLE_NAME, MarkerColumns._ID, contentValues);
|
||||
if (rowId >= 0) {
|
||||
return ContentUris.appendId(MarkerColumns.CONTENT_URI.buildUpon(), rowId).build();
|
||||
}
|
||||
throw new SQLException("Failed to insert a waypoint " + url);
|
||||
throw new SQLException("Failed to insert a marker " + url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Types of url.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
@VisibleForTesting
|
||||
enum UrlType {
|
||||
TRACKPOINTS,
|
||||
@@ -387,8 +369,8 @@ public class CustomContentProvider extends ContentProvider {
|
||||
TRACKPOINTS_BY_TRACKID,
|
||||
TRACKS,
|
||||
TRACKS_BY_ID,
|
||||
WAYPOINTS,
|
||||
WAYPOINTS_BY_ID,
|
||||
WAYPOINTS_BY_TRACKID
|
||||
MARKERS,
|
||||
MARKERS_BY_ID,
|
||||
MARKERS_BY_TRACKID
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
|
||||
|
||||
private static final int DATABASE_VERSION = 26;
|
||||
private static final int DATABASE_VERSION = 28;
|
||||
|
||||
@VisibleForTesting
|
||||
public static final String DATABASE_NAME = "database.db";
|
||||
@@ -70,6 +70,12 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
case 26:
|
||||
upgradeFrom25to26(db);
|
||||
break;
|
||||
case 27:
|
||||
upgradeFrom26to27(db);
|
||||
break;
|
||||
case 28:
|
||||
upgradeFrom27to28(db);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
|
||||
@@ -91,6 +97,12 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
case 25:
|
||||
downgradeFrom26to25(db);
|
||||
break;
|
||||
case 26:
|
||||
downgradeFrom27to26(db);
|
||||
break;
|
||||
case 27:
|
||||
downgradeFrom28to27(db);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
|
||||
@@ -135,6 +147,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add indeces for foreign key trackId
|
||||
*/
|
||||
private void upgradeFrom24to25(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
@@ -155,6 +170,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add track UUID to prevent re-import of existing tracks
|
||||
*/
|
||||
private void upgradeFrom25to26(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
@@ -166,7 +184,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
Track.Id trackId = new Track.Id(cursor.getLong(trackIdIndex));
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put("uuid", UUIDUtils.toBytes(UUID.randomUUID()));
|
||||
db.update("tracks", cv, "_id = ?", new String[]{String.valueOf(trackId)});
|
||||
db.update("tracks", cv, "_id = ?", new String[]{String.valueOf(trackId.getId())});
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
}
|
||||
@@ -190,4 +208,80 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add elevation gain
|
||||
*/
|
||||
private void upgradeFrom26to27(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
db.execSQL("ALTER TABLE trackpoints ADD COLUMN elevation_gain FLOAT");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
private void downgradeFrom27to26(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
|
||||
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT)");
|
||||
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power FROM trackpoints_old");
|
||||
db.execSQL("DROP TABLE trackpoints_old");
|
||||
|
||||
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add foreign key constraints on trackId
|
||||
*/
|
||||
private void upgradeFrom27to28(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
// TrackPoints
|
||||
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
|
||||
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
|
||||
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain FROM trackpoints_old");
|
||||
db.execSQL("DROP TABLE trackpoints_old");
|
||||
|
||||
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
|
||||
|
||||
// Markers
|
||||
db.execSQL("ALTER TABLE waypoints RENAME TO markers_old");
|
||||
db.execSQL("CREATE TABLE markers (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, icon TEXT, trackid INTEGER NOT NULL, length FLOAT, duration INTEGER, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, bearing FLOAT, photoUrl TEXT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
|
||||
|
||||
db.execSQL("INSERT INTO markers SELECT _id, name, description, category, icon, trackid, length, duration, longitude, latitude, time, elevation, accuracy, bearing, photoUrl FROM markers_old");
|
||||
db.execSQL("DROP TABLE markers_old");
|
||||
|
||||
db.execSQL("CREATE INDEX markers_trackid_index ON markers(trackid)");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
private void downgradeFrom28to27(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
// TrackPoints
|
||||
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
|
||||
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT)");
|
||||
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain FROM trackpoints_old");
|
||||
db.execSQL("DROP TABLE trackpoints_old");
|
||||
|
||||
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
|
||||
|
||||
// Markers
|
||||
db.execSQL("CREATE TABLE waypoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, icon TEXT, trackid INTEGER, length FLOAT, duration INTEGER, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, bearing FLOAT, photoUrl TEXT)");
|
||||
|
||||
db.execSQL("INSERT INTO waypoints SELECT _id, name, description, category, icon, trackid, length, duration, longitude, latitude, time, elevation, accuracy, bearing, photoUrl FROM markers");
|
||||
db.execSQL("DROP TABLE markers");
|
||||
|
||||
db.execSQL("CREATE INDEX waypoints_trackid_index ON waypoints(trackid)");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
@@ -19,11 +20,11 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import de.dennisguse.opentracks.android.IContentResolver;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
@@ -41,7 +42,7 @@ import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
||||
* While handling a request {@link ShareContentProvider} could `grantPermissions()` to the calling app for {@link CustomContentProvider}'s URI.
|
||||
* However, while handling the request this would allow the calling app to actually contact {@link CustomContentProvider} directly and get access to stored data that should remain private.
|
||||
*/
|
||||
public class ShareContentProvider extends CustomContentProvider implements IContentResolver {
|
||||
public class ShareContentProvider extends CustomContentProvider {
|
||||
|
||||
private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
|
||||
|
||||
@@ -209,12 +210,15 @@ public class ShareContentProvider extends CustomContentProvider implements ICont
|
||||
@Nullable
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(this);
|
||||
|
||||
Set<Track.Id> trackIds = parseURI(uri);
|
||||
final ArrayList<Track> tracks = new ArrayList<>();
|
||||
for (Track.Id trackId : trackIds) {
|
||||
tracks.add(contentProviderUtils.getTrack(trackId));
|
||||
String[] trackIdsString = trackIds.stream().map(Track.Id::toString).toArray(String[]::new);
|
||||
String whereClause = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
|
||||
|
||||
try (Cursor cursor = super.query(TracksColumns.CONTENT_URI, null, whereClause, trackIdsString, TracksColumns._ID)) {
|
||||
while (cursor.moveToNext()) {
|
||||
tracks.add(ContentProviderUtils.createTrack(cursor));
|
||||
}
|
||||
}
|
||||
|
||||
final TrackExporter trackExporter = getTrackFileFormat(uri).newTrackExporter(getContext());
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.dennisguse.opentracks.content.sensor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager;
|
||||
@@ -11,6 +13,10 @@ public class SensorData {
|
||||
|
||||
private final long timestamp_ms;
|
||||
|
||||
SensorData(String sensorAddress) {
|
||||
this(sensorAddress, null, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
SensorData(String sensorAddress, String sensorName) {
|
||||
this(sensorAddress, sensorName, System.currentTimeMillis());
|
||||
}
|
||||
@@ -22,14 +28,20 @@ public class SensorData {
|
||||
this.timestamp_ms = timestamp_ms;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getSensorAddress() {
|
||||
return sensorAddress;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSensorName() {
|
||||
return sensorName;
|
||||
}
|
||||
|
||||
public String getSensorNameOrAddress() {
|
||||
return sensorName != null ? sensorName : sensorAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the data recent considering the current time.
|
||||
*/
|
||||
|
||||
@@ -17,17 +17,20 @@ public final class SensorDataCycling {
|
||||
|
||||
private static final String TAG = SensorDataCycling.class.getSimpleName();
|
||||
|
||||
private static final int INVALID_VALUE_INT = -1;
|
||||
private static final float INVALID_VALUE_FLOAT = Float.NaN;
|
||||
|
||||
private SensorDataCycling() {
|
||||
}
|
||||
|
||||
public static class Cadence extends SensorData {
|
||||
|
||||
private final long crankRevolutionsCount; // UINT32
|
||||
private final int crankRevolutionsTime; // UINT16; 1/1024s
|
||||
private float cadence_rpm = INVALID_VALUE_FLOAT;
|
||||
private final Long crankRevolutionsCount; // UINT32
|
||||
private final Integer crankRevolutionsTime; // UINT16; 1/1024s
|
||||
private Float cadence_rpm;
|
||||
|
||||
public Cadence(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
this.crankRevolutionsCount = null;
|
||||
this.crankRevolutionsTime = null;
|
||||
}
|
||||
|
||||
public Cadence(String sensorAddress, String sensorName, long crankRevolutionsCount, int crankRevolutionsTime) {
|
||||
super(sensorAddress, sensorName);
|
||||
@@ -39,11 +42,11 @@ public final class SensorDataCycling {
|
||||
* Workaround for Wahoo CADENCE: provides speed instead of cadence
|
||||
*/
|
||||
public Cadence(@NonNull SensorDataCycling.Speed speed) {
|
||||
this(speed.getSensorAddress(), speed.getSensorName(), speed.getWheelRevolutionsCount(), speed.getWheelRevolutionsTime());
|
||||
this(speed.getSensorAddress(), speed.getSensorName(), speed.wheelRevolutionsCount, speed.wheelRevolutionsTime);
|
||||
}
|
||||
|
||||
public boolean hasData() {
|
||||
return crankRevolutionsCount != INVALID_VALUE_INT && crankRevolutionsTime != INVALID_VALUE_INT;
|
||||
return crankRevolutionsCount != null && crankRevolutionsTime != null;
|
||||
}
|
||||
|
||||
public long getCrankRevolutionsCount() {
|
||||
@@ -55,7 +58,7 @@ public final class SensorDataCycling {
|
||||
}
|
||||
|
||||
public boolean hasCadence_rpm() {
|
||||
return !Float.isNaN(cadence_rpm);
|
||||
return cadence_rpm != null;
|
||||
}
|
||||
|
||||
public float getCadence_rpm() {
|
||||
@@ -67,7 +70,7 @@ public final class SensorDataCycling {
|
||||
float timeDiff_ms = UintUtils.diff(crankRevolutionsTime, previous.crankRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
|
||||
if (timeDiff_ms <= 0) {
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
||||
cadence_rpm = INVALID_VALUE_FLOAT;
|
||||
cadence_rpm = null;
|
||||
} else {
|
||||
long crankDiff = UintUtils.diff(crankRevolutionsCount, previous.crankRevolutionsCount, UintUtils.UINT32_MAX);
|
||||
float cadence_ms = crankDiff / timeDiff_ms;
|
||||
@@ -79,7 +82,7 @@ public final class SensorDataCycling {
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "cadence=" + getCadence_rpm() + " time=" + getCrankRevolutionsTime() + " count=" + getCrankRevolutionsCount();
|
||||
return "cadence=" + cadence_rpm + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,15 +90,25 @@ public final class SensorDataCycling {
|
||||
if (!(obj instanceof Cadence)) return false;
|
||||
|
||||
Cadence comp = (Cadence) obj;
|
||||
return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime();
|
||||
if (hasData() && comp.hasData() == hasData()) {
|
||||
return getCrankRevolutionsCount() == comp.getCrankRevolutionsCount() && getCrankRevolutionsTime() == comp.getCrankRevolutionsTime();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Speed extends SensorData {
|
||||
|
||||
private final int wheelRevolutionsCount; // UINT16
|
||||
private final int wheelRevolutionsTime; // UINT16; 1/1024s
|
||||
private float speed_mps = INVALID_VALUE_FLOAT;
|
||||
private final Integer wheelRevolutionsCount; // UINT16
|
||||
private final Integer wheelRevolutionsTime; // UINT16; 1/1024s
|
||||
private Float speed_mps;
|
||||
|
||||
public Speed(String sensorAddress) {
|
||||
super(sensorAddress);
|
||||
this.wheelRevolutionsCount = null;
|
||||
this.wheelRevolutionsTime = null;
|
||||
}
|
||||
|
||||
public Speed(String sensorAddress, String sensorName, int wheelRevolutionsCount, int wheelRevolutionsTime) {
|
||||
super(sensorAddress, sensorName);
|
||||
@@ -104,7 +117,7 @@ public final class SensorDataCycling {
|
||||
}
|
||||
|
||||
public boolean hasData() {
|
||||
return wheelRevolutionsCount != INVALID_VALUE_INT && wheelRevolutionsTime != INVALID_VALUE_INT;
|
||||
return wheelRevolutionsCount != null && wheelRevolutionsTime != null;
|
||||
}
|
||||
|
||||
public int getWheelRevolutionsCount() {
|
||||
@@ -116,7 +129,7 @@ public final class SensorDataCycling {
|
||||
}
|
||||
|
||||
public boolean hasSpeed_mps() {
|
||||
return !Float.isNaN(speed_mps);
|
||||
return speed_mps != null;
|
||||
}
|
||||
|
||||
public float getSpeed_mps() {
|
||||
@@ -128,7 +141,7 @@ public final class SensorDataCycling {
|
||||
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
|
||||
if (timeDiff_ms <= 0) {
|
||||
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
|
||||
speed_mps = INVALID_VALUE_FLOAT;
|
||||
speed_mps = null;
|
||||
} else {
|
||||
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
|
||||
double timeDiff_s = timeDiff_ms * UnitConversions.MS_TO_S;
|
||||
@@ -140,7 +153,7 @@ public final class SensorDataCycling {
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "speed=" + getSpeed_mps() + " time=" + getWheelRevolutionsTime() + " count=" + getWheelRevolutionsCount();
|
||||
return "speed=" + speed_mps + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -148,7 +161,11 @@ public final class SensorDataCycling {
|
||||
if (!(obj instanceof Speed)) return false;
|
||||
|
||||
Speed comp = (Speed) obj;
|
||||
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
|
||||
if (hasData() && comp.hasData() == hasData()) {
|
||||
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package de.dennisguse.opentracks.content.sensor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class SensorDataCyclingPower extends SensorData {
|
||||
|
||||
private final Float power_w;
|
||||
|
||||
public SensorDataCyclingPower(String address) {
|
||||
super(address);
|
||||
this.power_w = null;
|
||||
}
|
||||
|
||||
public SensorDataCyclingPower(String name, String address, float power_w) {
|
||||
super(name, address);
|
||||
this.power_w = power_w;
|
||||
}
|
||||
|
||||
public boolean hasPower_w() {
|
||||
return power_w != null;
|
||||
}
|
||||
|
||||
public float getPower_w() {
|
||||
return power_w;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "power=" + power_w;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,12 @@ import androidx.annotation.NonNull;
|
||||
|
||||
public class SensorDataHeartRate extends SensorData {
|
||||
|
||||
private final float heartRate_bpm;
|
||||
private final Float heartRate_bpm;
|
||||
|
||||
public SensorDataHeartRate(String address) {
|
||||
super(address);
|
||||
heartRate_bpm = null;
|
||||
}
|
||||
|
||||
public SensorDataHeartRate(String name, String address, float heartRate_bpm) {
|
||||
super(name, address);
|
||||
@@ -12,10 +17,10 @@ public class SensorDataHeartRate extends SensorData {
|
||||
}
|
||||
|
||||
public boolean hasHeartRate_bpm() {
|
||||
return !Float.isNaN(heartRate_bpm);
|
||||
return heartRate_bpm != null;
|
||||
}
|
||||
|
||||
public float getHeartRate_bpm() {
|
||||
public Float getHeartRate_bpm() {
|
||||
return heartRate_bpm;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ public final class SensorDataSet {
|
||||
|
||||
private SensorDataCycling.Speed cyclingSpeed;
|
||||
|
||||
private SensorDataCyclingPower cyclingPower;
|
||||
|
||||
public SensorDataSet() {
|
||||
}
|
||||
|
||||
@@ -27,28 +29,23 @@ public final class SensorDataSet {
|
||||
return cyclingSpeed;
|
||||
}
|
||||
|
||||
public SensorDataCyclingPower getCyclingPower() {
|
||||
return cyclingPower;
|
||||
}
|
||||
|
||||
public void set(SensorData data) {
|
||||
if (data instanceof SensorDataHeartRate) {
|
||||
this.heartRate = (SensorDataHeartRate) data;
|
||||
return;
|
||||
}
|
||||
set(data, data);
|
||||
}
|
||||
|
||||
if (data instanceof SensorDataCycling.Cadence) {
|
||||
this.cyclingCadence = (SensorDataCycling.Cadence) data;
|
||||
return;
|
||||
}
|
||||
if (data instanceof SensorDataCycling.Speed) {
|
||||
this.cyclingSpeed = (SensorDataCycling.Speed) data;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException();
|
||||
public void remove(SensorData type) {
|
||||
set(type, null);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.heartRate = null;
|
||||
this.cyclingCadence = null;
|
||||
this.cyclingSpeed = null;
|
||||
this.cyclingPower = null;
|
||||
}
|
||||
|
||||
public void fillTrackPoint(TrackPoint trackPoint) {
|
||||
@@ -63,6 +60,10 @@ public final class SensorDataSet {
|
||||
if (cyclingSpeed != null && cyclingSpeed.hasSpeed_mps()) {
|
||||
trackPoint.setSpeed(cyclingSpeed.getSpeed_mps());
|
||||
}
|
||||
|
||||
if (cyclingPower != null && cyclingPower.hasPower_w()) {
|
||||
trackPoint.setPower(cyclingPower.getPower_w());
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -70,6 +71,30 @@ public final class SensorDataSet {
|
||||
public String toString() {
|
||||
return (getHeartRate() != null ? "" + getHeartRate() : "")
|
||||
+ (getCyclingCadence() != null ? " " + getCyclingCadence() : "")
|
||||
+ (getCyclingSpeed() != null ? " " + getCyclingSpeed() : "");
|
||||
+ (getCyclingSpeed() != null ? " " + getCyclingSpeed() : "")
|
||||
+ (getCyclingPower() != null ? " " + getCyclingPower() : "");
|
||||
}
|
||||
|
||||
private void set(@NonNull SensorData type, SensorData data) {
|
||||
if (type instanceof SensorDataHeartRate) {
|
||||
this.heartRate = (SensorDataHeartRate) data;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type instanceof SensorDataCycling.Cadence) {
|
||||
this.cyclingCadence = (SensorDataCycling.Cadence) data;
|
||||
return;
|
||||
}
|
||||
if (type instanceof SensorDataCycling.Speed) {
|
||||
this.cyclingSpeed = (SensorDataCycling.Speed) data;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type instanceof SensorDataCyclingPower) {
|
||||
this.cyclingPower = (SensorDataCyclingPower) data;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException(type.getClass().getCanonicalName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import de.dennisguse.opentracks.content.TrackDataListener;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.databinding.ChartBinding;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.util.LocationUtils;
|
||||
@@ -73,7 +74,10 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
private TrackStatisticsUpdater trackStatisticsUpdater;
|
||||
private long startTime;
|
||||
|
||||
//TODO Why is this needed?
|
||||
private ChartBinding viewBinding;
|
||||
|
||||
//TODO REMOVE: Used to restore the TrackStatistics via TrackStatisticsUpdater; for the diagrams we should consider not this setting and just use all data that is in the database
|
||||
@Deprecated
|
||||
private int recordingDistanceInterval;
|
||||
private String category = "";
|
||||
|
||||
@@ -147,15 +151,16 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.chart, container, false);
|
||||
viewBinding = ChartBinding.inflate(inflater, container, false);
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
ViewGroup layout = getView().findViewById(R.id.chart_view_layout);
|
||||
|
||||
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
||||
layout.addView(chartView, layoutParams);
|
||||
viewBinding.chartViewLayout.addView(chartView, layoutParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -179,8 +184,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
ViewGroup layout = getView().findViewById(R.id.chart_view_layout);
|
||||
layout.removeView(chartView);
|
||||
|
||||
viewBinding.chartViewLayout.removeView(chartView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
viewBinding = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -244,14 +255,14 @@ public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
@Override
|
||||
public void clearMarkers() {
|
||||
if (isResumed()) {
|
||||
chartView.clearWaypoints();
|
||||
chartView.clearMarker();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewMarker(Marker marker) {
|
||||
if (isResumed() && marker != null && LocationUtils.isValidLocation(marker.getLocation())) {
|
||||
chartView.addWaypoint(marker);
|
||||
chartView.addMarker(marker);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.GridView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
@@ -17,6 +16,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.databinding.ChooseActivityTypeBinding;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
|
||||
/**
|
||||
@@ -39,6 +39,8 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
|
||||
return TrackIconUtils.getAllIconValues().indexOf(iconValue);
|
||||
}
|
||||
|
||||
private ChooseActivityTypeBinding viewBinding;
|
||||
|
||||
private final String preselectedCategory;
|
||||
|
||||
private ChooseActivityTypeCaller chooseActivityTypeCaller;
|
||||
@@ -57,8 +59,7 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.choose_activity_type, container);
|
||||
GridView gridView = view.findViewById(R.id.choose_activity_type_grid_view);
|
||||
viewBinding = ChooseActivityTypeBinding.inflate(inflater, container, false);
|
||||
|
||||
List<Integer> imageIds = new ArrayList<>();
|
||||
for (String iconValue : TrackIconUtils.getAllIconValues()) {
|
||||
@@ -66,15 +67,14 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
|
||||
}
|
||||
|
||||
final ChooseActivityTypeImageAdapter imageAdapter = new ChooseActivityTypeImageAdapter(imageIds);
|
||||
gridView.setAdapter(imageAdapter);
|
||||
|
||||
int position = getPosition(getContext(), preselectedCategory);
|
||||
if (position != -1) {
|
||||
imageAdapter.setSelected(position);
|
||||
}
|
||||
|
||||
gridView.setOnItemClickListener(this);
|
||||
return view;
|
||||
viewBinding.chooseActivityTypeGridView.setAdapter(imageAdapter);
|
||||
viewBinding.chooseActivityTypeGridView.setOnItemClickListener(this);
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
public void onAttach(@NonNull Context context) {
|
||||
@@ -86,6 +86,11 @@ public class ChooseActivityTypeDialogFragment extends DialogFragment implements
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
viewBinding = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
|
||||
|
||||
@@ -22,6 +22,8 @@ import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
@@ -76,7 +78,7 @@ class ChooseActivityTypeImageAdapter extends BaseAdapter {
|
||||
}
|
||||
|
||||
if (position == selected) {
|
||||
imageView.setBackgroundResource(R.drawable.list_selector_background_transition_holo_dark);
|
||||
imageView.setBackgroundColor(ContextCompat.getColor(parent.getContext(), R.color.colorAccent));
|
||||
} else {
|
||||
imageView.setBackgroundColor(Color.TRANSPARENT);
|
||||
}
|
||||
|
||||
@@ -42,9 +42,9 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
|
||||
|
||||
private DeleteMarkerCaller caller;
|
||||
|
||||
public static void showDialog(FragmentManager fragmentManager, Marker.Id... waypointIds) {
|
||||
public static void showDialog(FragmentManager fragmentManager, Marker.Id... markerIds) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArray(KEY_MARKER_IDS, waypointIds);
|
||||
bundle.putParcelableArray(KEY_MARKER_IDS, markerIds);
|
||||
|
||||
DeleteMarkerDialogFragment deleteMarkerDialogFragment = new DeleteMarkerDialogFragment();
|
||||
deleteMarkerDialogFragment.setArguments(bundle);
|
||||
@@ -65,24 +65,18 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Marker.Id[] markerIds = (Marker.Id[]) getArguments().getParcelableArray(KEY_MARKER_IDS);
|
||||
final Context context = getContext();
|
||||
|
||||
final FragmentActivity fragmentActivity = getActivity();
|
||||
int titleId;
|
||||
int messageId;
|
||||
if (markerIds == null) {
|
||||
titleId = R.string.generic_delete_all_confirm_title;
|
||||
messageId = R.string.marker_delete_all_confirm_message;
|
||||
} else {
|
||||
titleId = markerIds.length > 1 ? R.string.generic_delete_selected_confirm_title : R.string.marker_delete_one_confirm_title;
|
||||
messageId = markerIds.length > 1 ? R.string.marker_delete_multiple_confirm_message : R.string.marker_delete_one_confirm_message;
|
||||
}
|
||||
int titleId = markerIds.length > 1 ? R.string.generic_delete_selected_confirm_title : R.string.marker_delete_one_confirm_title;
|
||||
int messageId = markerIds.length > 1 ? R.string.marker_delete_multiple_confirm_message : R.string.marker_delete_one_confirm_message;
|
||||
return DialogUtils.createConfirmationDialog(
|
||||
fragmentActivity, titleId, getString(messageId), (dialog, which) -> new Thread(() -> {
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(fragmentActivity);
|
||||
for (Marker.Id markerId : markerIds) {
|
||||
contentProviderUtils.deleteMarker(getContext(), markerId);
|
||||
contentProviderUtils.deleteMarker(context, markerId);
|
||||
}
|
||||
caller.onDeleteMarkerDone();
|
||||
caller.onMarkerDeleted();
|
||||
}).start());
|
||||
}
|
||||
|
||||
@@ -96,6 +90,6 @@ public class DeleteMarkerDialogFragment extends DialogFragment {
|
||||
/**
|
||||
* Called when delete marker is done.
|
||||
*/
|
||||
void onDeleteMarkerDone();
|
||||
void onMarkerDeleted();
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
|
||||
public class ExportFinishedDialogFragment extends DialogFragment {
|
||||
|
||||
private static final String TAG = ExportFinishedDialogFragment.class.getSimpleName();
|
||||
|
||||
private static final String EXTRA_DIRECTORY_NAME_KEY = "directory_name";
|
||||
private static final String EXTRA_EXPORTED_TRACK_COUNT = "exported_track_count";
|
||||
private static final String EXTRA_TOTAL_TRACK_COUNT = "";
|
||||
|
||||
private static final String FINISHED_DIALOG_TAG = "finished_dialog_tag";
|
||||
|
||||
private DismissCallback caller;
|
||||
|
||||
public static void showDialog(FragmentManager fragmentManager, String directoryDisplayName, int successCount, int fileCount) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(EXTRA_DIRECTORY_NAME_KEY, directoryDisplayName);
|
||||
bundle.putInt(EXTRA_EXPORTED_TRACK_COUNT, successCount);
|
||||
bundle.putInt(EXTRA_TOTAL_TRACK_COUNT, fileCount);
|
||||
|
||||
ExportFinishedDialogFragment dialogFragment = new ExportFinishedDialogFragment();
|
||||
dialogFragment.setArguments(bundle);
|
||||
dialogFragment.show(fragmentManager, FINISHED_DIALOG_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
try {
|
||||
caller = (DismissCallback) context;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
String directoryDisplayName = getArguments().getString(EXTRA_DIRECTORY_NAME_KEY);
|
||||
int exportedTracksCount = getArguments().getInt(EXTRA_EXPORTED_TRACK_COUNT);
|
||||
int totalTracksCount = getArguments().getInt(EXTRA_TOTAL_TRACK_COUNT);
|
||||
|
||||
int iconId;
|
||||
int titleId;
|
||||
String message;
|
||||
String totalTracks = getResources().getQuantityString(R.plurals.tracks, totalTracksCount, totalTracksCount);
|
||||
if (exportedTracksCount == totalTracksCount) {
|
||||
iconId = R.drawable.ic_dialog_success_24dp;
|
||||
titleId = R.string.generic_success_title;
|
||||
message = getString(
|
||||
R.string.export_success, totalTracks, directoryDisplayName);
|
||||
} else {
|
||||
iconId = R.drawable.ic_dialog_error_24dp;
|
||||
titleId = R.string.generic_error_title;
|
||||
message = getString(R.string.export_error, exportedTracksCount, totalTracks, directoryDisplayName);
|
||||
}
|
||||
Dialog alertDialog = new AlertDialog.Builder(getContext()).setCancelable(true)
|
||||
.setIcon(iconId).setMessage(message)
|
||||
.setPositiveButton(R.string.generic_ok, (dialog, arg1) -> {
|
||||
dialog.dismiss();
|
||||
caller.onDismissed();
|
||||
}).setTitle(titleId)
|
||||
.create();
|
||||
|
||||
alertDialog.setCanceledOnTouchOutside(false);
|
||||
return alertDialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
caller = null;
|
||||
}
|
||||
|
||||
public interface DismissCallback {
|
||||
void onDismissed();
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
|
||||
import de.dennisguse.opentracks.util.DialogUtils;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
public class ExportProgressDialogFragment extends DialogFragment {
|
||||
|
||||
private static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
|
||||
private static final String EXTRA_TRACKFILEFORMAT_KEY = "trackfileformat";
|
||||
|
||||
private static final String TAG = ExportProgressDialogFragment.class.getSimpleName();
|
||||
|
||||
public static final String EXPORT_DIALOG_TAG = "export_dialog_tag";
|
||||
|
||||
private DismissCallback caller;
|
||||
|
||||
private ExportThread exportThread;
|
||||
|
||||
private String directoryDisplayName;
|
||||
|
||||
private int trackExportSuccessCount;
|
||||
private int trackCount;
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*/
|
||||
public static void showDialog(FragmentManager fragmentManager, TrackFileFormat trackFileFormat, Uri documentFileUri) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(EXTRA_DIRECTORY_URI_KEY, documentFileUri);
|
||||
bundle.putSerializable(EXTRA_TRACKFILEFORMAT_KEY, trackFileFormat);
|
||||
|
||||
ExportProgressDialogFragment dialogFragment = new ExportProgressDialogFragment();
|
||||
dialogFragment.setArguments(bundle);
|
||||
dialogFragment.setRetainInstance(true);
|
||||
dialogFragment.show(fragmentManager, EXPORT_DIALOG_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// This fragment uses a background thread, so it should not be recreated during activity re-creation.
|
||||
setRetainInstance(true);
|
||||
|
||||
Uri directoryUri = getArguments().getParcelable(EXTRA_DIRECTORY_URI_KEY);
|
||||
TrackFileFormat trackFileFormat = (TrackFileFormat) getArguments().getSerializable(EXTRA_TRACKFILEFORMAT_KEY);
|
||||
DocumentFile documentFile = DocumentFile.fromTreeUri(getContext(), directoryUri);
|
||||
|
||||
directoryDisplayName = FileUtils.getPath(documentFile);
|
||||
|
||||
exportThread = new ExportThread(trackFileFormat, documentFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
try {
|
||||
caller = (DismissCallback) context;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
ProgressDialog progressDialog = DialogUtils.createHorizontalProgressDialog(getContext(),
|
||||
R.string.export_progress_message,
|
||||
dialog -> {
|
||||
exportThread.interrupt();
|
||||
dialog.dismiss();
|
||||
caller.onExportCanceled(directoryDisplayName, trackExportSuccessCount, trackCount);
|
||||
}, directoryDisplayName);
|
||||
|
||||
progressDialog.setIndeterminate(false);
|
||||
progressDialog.setMax(trackCount);
|
||||
progressDialog.setProgress(Math.min(trackExportSuccessCount, trackCount));
|
||||
|
||||
return progressDialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
setProgress();
|
||||
if (!exportThread.isAlive()) {
|
||||
exportThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (!exportThread.isAlive()) {
|
||||
exportThread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
caller = null;
|
||||
}
|
||||
|
||||
private void onProgress(int trackImportSuccessCount, int fileCount) {
|
||||
this.trackExportSuccessCount = trackImportSuccessCount;
|
||||
this.trackCount = fileCount;
|
||||
setProgress();
|
||||
}
|
||||
|
||||
private void setProgress() {
|
||||
ProgressDialog dialog = (ProgressDialog) getDialog();
|
||||
if (dialog != null) {
|
||||
dialog.setIndeterminate(false);
|
||||
dialog.setMax(trackCount);
|
||||
dialog.setProgress(Math.min(trackExportSuccessCount, trackCount));
|
||||
}
|
||||
}
|
||||
|
||||
private void onExportCompleted(int successCount, int fileCount) {
|
||||
if (caller != null && !isDetached()) {
|
||||
caller.onExportFinished(directoryDisplayName, successCount, fileCount);
|
||||
}
|
||||
dismissAllowingStateLoss();
|
||||
}
|
||||
|
||||
private class ExportThread extends Thread {
|
||||
|
||||
private final TrackFileFormat trackFileFormat;
|
||||
|
||||
private final DocumentFile directory;
|
||||
|
||||
ExportThread(TrackFileFormat trackFileFormat, DocumentFile directory) {
|
||||
this.directory = directory;
|
||||
this.trackFileFormat = trackFileFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Context context = ExportProgressDialogFragment.this.getContext();
|
||||
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
|
||||
|
||||
try (Cursor cursor = contentProviderUtils.getTrackCursor(null, null, TracksColumns._ID)) {
|
||||
if (cursor == null) {
|
||||
onExportCompleted(0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
trackCount = cursor.getCount();
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
onProgress(i, trackCount);
|
||||
if (Thread.interrupted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.moveToPosition(i);
|
||||
Track track = contentProviderUtils.createTrack(cursor);
|
||||
if (track != null && exportTrack(context, track)) {
|
||||
trackExportSuccessCount++;
|
||||
}
|
||||
}
|
||||
|
||||
onExportCompleted(trackExportSuccessCount, trackCount);
|
||||
}
|
||||
}
|
||||
|
||||
private Boolean exportTrack(Context context, Track track) {
|
||||
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context);
|
||||
|
||||
//TODO Move to helper function
|
||||
String fileName = track.getId().getId() + "." + trackFileFormat.getExtension();
|
||||
|
||||
// Overwrite a file if it exists; DocumentFile.createFile() creates a new file appending a suffix if the displayname already exists.
|
||||
DocumentFile file = directory.findFile(fileName);
|
||||
if (file == null) {
|
||||
file = directory.createFile(trackFileFormat.getMimeType(), fileName);
|
||||
}
|
||||
|
||||
try (OutputStream outputStream = context.getContentResolver().openOutputStream(file.getUri())) {
|
||||
if (trackExporter.writeTrack(track, outputStream)) {
|
||||
return true;
|
||||
} else {
|
||||
if (!file.delete()) {
|
||||
Log.e(TAG, "Unable to delete file");
|
||||
}
|
||||
Log.e(TAG, "Unable to export track");
|
||||
return false;
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.e(TAG, "Unable to open file " + file.getName(), e);
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to close file output stream", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface DismissCallback {
|
||||
void onExportCanceled(String directoryDisplayName, int successCount, int fileCount);
|
||||
|
||||
void onExportFinished(String directoryDisplayName, int successCount, int fileCount);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
|
||||
public class ImportFinishedDialogFragment extends DialogFragment {
|
||||
|
||||
private static final String TAG = ImportFinishedDialogFragment.class.getSimpleName();
|
||||
|
||||
private static final String EXTRA_DIRECTORY_NAME_KEY = "directory_name";
|
||||
private static final String EXTRA_IMPORTED_TRACK_COUNT = "imported_track_count";
|
||||
private static final String EXTRA_FILE_COUNT = "file_count";
|
||||
|
||||
private static final String FINISHED_DIALOG_TAG = "finished_dialog_tag";
|
||||
|
||||
private DismissCallback caller;
|
||||
|
||||
public static void showDialog(FragmentManager fragmentManager, String directoryDisplayName, int successCount, int fileCount) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(EXTRA_DIRECTORY_NAME_KEY, directoryDisplayName);
|
||||
bundle.putInt(EXTRA_IMPORTED_TRACK_COUNT, successCount);
|
||||
bundle.putInt(EXTRA_FILE_COUNT, fileCount);
|
||||
|
||||
ImportFinishedDialogFragment dialogFragment = new ImportFinishedDialogFragment();
|
||||
dialogFragment.setArguments(bundle);
|
||||
dialogFragment.show(fragmentManager, FINISHED_DIALOG_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
try {
|
||||
caller = (DismissCallback) context;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
String directoryDisplayName = getArguments().getString(EXTRA_DIRECTORY_NAME_KEY);
|
||||
int importedTrackCount = getArguments().getInt(EXTRA_IMPORTED_TRACK_COUNT);
|
||||
int totalTrackCount = getArguments().getInt(EXTRA_FILE_COUNT);
|
||||
|
||||
int iconId;
|
||||
int titleId;
|
||||
String message;
|
||||
String totalFiles = getResources().getQuantityString(R.plurals.files, totalTrackCount, totalTrackCount);
|
||||
if (importedTrackCount == totalTrackCount) {
|
||||
if (totalTrackCount == 0) {
|
||||
iconId = R.drawable.ic_dialog_info_24dp;
|
||||
titleId = R.string.import_no_file_title;
|
||||
message = getString(R.string.import_no_file, directoryDisplayName);
|
||||
} else {
|
||||
iconId = R.drawable.ic_dialog_success_24dp;
|
||||
titleId = R.string.generic_success_title;
|
||||
message = getString(R.string.import_success, totalFiles, directoryDisplayName);
|
||||
}
|
||||
} else {
|
||||
iconId = R.drawable.ic_dialog_error_24dp;
|
||||
titleId = R.string.generic_error_title;
|
||||
message = getString(R.string.import_error, importedTrackCount, totalFiles, directoryDisplayName);
|
||||
}
|
||||
Dialog dialog = new AlertDialog.Builder(getContext())
|
||||
.setCancelable(true).setIcon(iconId)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.generic_ok, (dialogInterface, which) -> {
|
||||
dialogInterface.dismiss();
|
||||
caller.onDismissed();
|
||||
})
|
||||
.setTitle(titleId)
|
||||
.create();
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
caller = null;
|
||||
}
|
||||
|
||||
public interface DismissCallback {
|
||||
void onDismissed();
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.io.file.importer.GpxFileTrackImporter;
|
||||
import de.dennisguse.opentracks.io.file.importer.KmlFileTrackImporter;
|
||||
import de.dennisguse.opentracks.io.file.importer.KmzTrackImporter;
|
||||
import de.dennisguse.opentracks.io.file.importer.TrackImporter;
|
||||
import de.dennisguse.opentracks.util.DialogUtils;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
public class ImportProgressDialogFragment extends DialogFragment {
|
||||
|
||||
private static final String EXTRA_IS_TREE_KEY = "tree_uri";
|
||||
|
||||
private static final String EXTRA_DATA_URI_KEY = "data_uri";
|
||||
|
||||
private static final String TAG = ImportProgressDialogFragment.class.getSimpleName();
|
||||
|
||||
public static final String IMPORT_DIALOG_TAG = "import_dialog_tag";
|
||||
|
||||
private DismissCallback caller;
|
||||
|
||||
private ImportThread importThread;
|
||||
|
||||
private String directoryDisplayName;
|
||||
|
||||
private int trackImportSuccessCount;
|
||||
private int fileCount;
|
||||
|
||||
public static void showDialog(FragmentManager fragmentManager, Uri documentFileUri, boolean isDirectory) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(EXTRA_DATA_URI_KEY, documentFileUri);
|
||||
bundle.putBoolean(EXTRA_IS_TREE_KEY, isDirectory);
|
||||
|
||||
ImportProgressDialogFragment dialogFragment = new ImportProgressDialogFragment();
|
||||
dialogFragment.setArguments(bundle);
|
||||
dialogFragment.setRetainInstance(true);
|
||||
dialogFragment.show(fragmentManager, IMPORT_DIALOG_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// This fragment uses a background thread, so it should not be recreated during activity re-creation.
|
||||
setRetainInstance(true);
|
||||
|
||||
Uri directoryUri = getArguments().getParcelable(EXTRA_DATA_URI_KEY);
|
||||
DocumentFile documentFile;
|
||||
if (getArguments().getBoolean(EXTRA_IS_TREE_KEY)) {
|
||||
documentFile = DocumentFile.fromTreeUri(getContext(), directoryUri);
|
||||
} else {
|
||||
documentFile = DocumentFile.fromSingleUri(getContext(), directoryUri);
|
||||
}
|
||||
directoryDisplayName = FileUtils.getPath(documentFile);
|
||||
|
||||
importThread = new ImportThread(documentFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
try {
|
||||
caller = (DismissCallback) context;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
ProgressDialog progressDialog = DialogUtils.createHorizontalProgressDialog(
|
||||
getContext(), R.string.import_progress_message,
|
||||
dialogInterface -> {
|
||||
importThread.interrupt();
|
||||
dialogInterface.dismiss();
|
||||
caller.onImportCanceled(directoryDisplayName, trackImportSuccessCount, fileCount);
|
||||
}, directoryDisplayName);
|
||||
|
||||
progressDialog.setIndeterminate(false);
|
||||
progressDialog.setMax(fileCount);
|
||||
progressDialog.setProgress(Math.min(trackImportSuccessCount, fileCount));
|
||||
|
||||
return progressDialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
setProgress();
|
||||
if (!importThread.isAlive()) {
|
||||
importThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (!importThread.isAlive()) {
|
||||
importThread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach() {
|
||||
super.onDetach();
|
||||
caller = null;
|
||||
}
|
||||
|
||||
private void onProgress(int trackImportSuccessCount, int fileCount) {
|
||||
this.trackImportSuccessCount = trackImportSuccessCount;
|
||||
this.fileCount = fileCount;
|
||||
setProgress();
|
||||
}
|
||||
|
||||
private void setProgress() {
|
||||
ProgressDialog dialog = (ProgressDialog) getDialog();
|
||||
if (dialog != null) {
|
||||
dialog.setIndeterminate(false);
|
||||
dialog.setMax(fileCount);
|
||||
dialog.setProgress(Math.min(trackImportSuccessCount, fileCount));
|
||||
}
|
||||
}
|
||||
|
||||
private void onImportCompleted(int successCount, int fileCount) {
|
||||
if (caller != null && !isDetached()) {
|
||||
caller.onImportFinished(directoryDisplayName, successCount, fileCount);
|
||||
}
|
||||
dismissAllowingStateLoss();
|
||||
}
|
||||
|
||||
private class ImportThread extends Thread {
|
||||
|
||||
private final DocumentFile file;
|
||||
|
||||
ImportThread(DocumentFile file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Context context = ImportProgressDialogFragment.this.getContext();
|
||||
|
||||
List<DocumentFile> files = FileUtils.getFiles(file);
|
||||
|
||||
int importedTrackCount = 0;
|
||||
for (int i = 0; i < files.size(); i++) {
|
||||
onProgress(i, files.size());
|
||||
|
||||
if (Thread.interrupted()) {
|
||||
return;
|
||||
}
|
||||
if (importFile(context, files.get(i))) {
|
||||
importedTrackCount++;
|
||||
}
|
||||
}
|
||||
|
||||
onImportCompleted(importedTrackCount, files.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a file.
|
||||
*
|
||||
* @param file the file
|
||||
*/
|
||||
private boolean importFile(final Context context, final DocumentFile file) {
|
||||
TrackImporter trackImporter;
|
||||
String fileExtension = FileUtils.getExtension(file);
|
||||
|
||||
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
|
||||
trackImporter = new GpxFileTrackImporter(context);
|
||||
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
|
||||
trackImporter = new KmlFileTrackImporter(ImportProgressDialogFragment.this.getContext());
|
||||
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
|
||||
trackImporter = new KmzTrackImporter(context, file.getUri());
|
||||
} else {
|
||||
Log.i(TAG, "Unsupported file format.");
|
||||
return false;
|
||||
}
|
||||
|
||||
try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) {
|
||||
return trackImporter.importFile(inputStream) != null;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface DismissCallback {
|
||||
void onImportCanceled(String directoryDisplayName, int successCount, int fileCount);
|
||||
|
||||
void onImportFinished(String directoryDisplayName, int successCount, int fileCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackActivityDataHubInterface;
|
||||
import de.dennisguse.opentracks.adapters.IntervalStatisticsAdapter;
|
||||
import de.dennisguse.opentracks.content.TrackDataHub;
|
||||
import de.dennisguse.opentracks.content.TrackDataListener;
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.databinding.IntervalListViewBinding;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
|
||||
import de.dennisguse.opentracks.viewmodels.IntervalStatisticsModel;
|
||||
|
||||
/**
|
||||
* A fragment to display the intervals from recorded track.
|
||||
*/
|
||||
public class IntervalsFragment extends Fragment implements TrackDataListener {
|
||||
|
||||
private static final String TAG = IntervalsFragment.class.getSimpleName();
|
||||
|
||||
private IntervalStatisticsModel viewModel;
|
||||
protected IntervalStatisticsAdapter.StackMode stackModeListView;
|
||||
private IntervalStatisticsModel.IntervalOption selectedInterval;
|
||||
|
||||
private String intervalUnit;
|
||||
private IntervalStatisticsAdapter adapter;
|
||||
private ArrayAdapter<IntervalStatisticsModel.IntervalOption> spinnerAdapter;
|
||||
|
||||
private TrackDataHub trackDataHub;
|
||||
private String category;
|
||||
|
||||
private IntervalListViewBinding viewBinding;
|
||||
|
||||
protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (preferences, key) -> {
|
||||
if (PreferencesUtils.isKey(getContext(), R.string.stats_units_key, key) || PreferencesUtils.isKey(getContext(), R.string.stats_rate_key, key)) {
|
||||
intervalUnit = PreferencesUtils.isMetricUnits(getContext()) ? getContext().getString(R.string.unit_kilometer) : getContext().getString(R.string.unit_mile);
|
||||
if (adapter != null) {
|
||||
adapter.notifyDataSetChanged();
|
||||
spinnerAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static Fragment newInstance() {
|
||||
return new IntervalsFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
viewBinding = IntervalListViewBinding.inflate(inflater, container, false);
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
PreferencesUtils.register(getContext(), sharedPreferenceChangeListener);
|
||||
intervalUnit = PreferencesUtils.isMetricUnits(getContext()) ? getContext().getString(R.string.unit_kilometer) : getContext().getString(R.string.unit_mile);
|
||||
|
||||
viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
|
||||
|
||||
stackModeListView = IntervalStatisticsAdapter.StackMode.STACK_FROM_TOP;
|
||||
|
||||
viewModel = new IntervalStatisticsModel();
|
||||
|
||||
spinnerAdapter = new ArrayAdapter<IntervalStatisticsModel.IntervalOption>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
TextView v = (TextView) super.getView(position, convertView, parent);
|
||||
v.setText(v.getText() + " " + intervalUnit);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
TextView v = (TextView) super.getDropDownView(position, convertView, parent);
|
||||
v.setText(v.getText() + " " + intervalUnit);
|
||||
return v;
|
||||
}
|
||||
};
|
||||
|
||||
viewBinding.spinnerIntervals.setAdapter(spinnerAdapter);
|
||||
viewBinding.spinnerIntervals.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
|
||||
selectedInterval = IntervalStatisticsModel.IntervalOption.values()[i];
|
||||
loadIntervals();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> adapterView) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
resumeTrackDataHub();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
pauseTrackDataHub();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
viewBinding = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
PreferencesUtils.unregister(getContext(), sharedPreferenceChangeListener);
|
||||
|
||||
adapter = null;
|
||||
viewModel = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update intervals through {@link IntervalStatisticsModel} view model.
|
||||
*/
|
||||
protected synchronized void loadIntervals() {
|
||||
if (viewModel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(getContext());
|
||||
IntervalStatistics intervalStatistics = viewModel.getIntervalStats(metricUnits, selectedInterval);
|
||||
adapter = new IntervalStatisticsAdapter(getContext(), intervalStatistics.getIntervalList(), category, stackModeListView);
|
||||
viewBinding.intervalList.setAdapter(adapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the trackDataHub.
|
||||
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void resumeTrackDataHub() {
|
||||
trackDataHub = ((TrackActivityDataHubInterface) getActivity()).getTrackDataHub();
|
||||
trackDataHub.registerTrackDataListener(this, true, false, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the trackDataHub.
|
||||
* Needs to be synchronized because trackDataHub can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void pauseTrackDataHub() {
|
||||
trackDataHub.unregisterTrackDataListener(this);
|
||||
trackDataHub = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrackUpdated(Track track) {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(() -> {
|
||||
if (isResumed()) {
|
||||
// Set category.
|
||||
category = track != null ? track.getCategory() : "";
|
||||
|
||||
// Set rate label.
|
||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext(), category);
|
||||
viewBinding.intervalRate.setText(reportSpeed ? R.string.stats_speed : R.string.stats_pace);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearTrackPoints() {
|
||||
if (isResumed()) {
|
||||
viewModel.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledInTrackPoint(TrackPoint trackPoint) {
|
||||
if (isResumed()) {
|
||||
viewModel.add(trackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledOutTrackPoint(TrackPoint trackPoint) {
|
||||
if (isResumed()) {
|
||||
viewModel.add(trackPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewTrackPointsDone() {
|
||||
if (isResumed()) {
|
||||
runOnUiThread(this::loadIntervals);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearMarkers() {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewMarker(Marker marker) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewMarkersDone() {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a runnable on the UI thread.
|
||||
*
|
||||
* @param runnable the runnable
|
||||
*/
|
||||
private void runOnUiThread(Runnable runnable) {
|
||||
FragmentActivity fragmentActivity = getActivity();
|
||||
if (fragmentActivity != null) {
|
||||
fragmentActivity.runOnUiThread(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
public static class IntervalsRecordingFragment extends IntervalsFragment {
|
||||
// Refreshing intervals stats it's not so demanding so 5 seconds is enough to balance performance and user experience.
|
||||
private static final long UI_UPDATE_INTERVAL = 5 * UnitConversions.ONE_SECOND_MS;
|
||||
|
||||
private Handler intervalHandler;
|
||||
|
||||
private final Runnable intervalRunner = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
updateIntervals();
|
||||
intervalHandler.postDelayed(intervalRunner, UI_UPDATE_INTERVAL);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static Fragment newInstance() {
|
||||
return new IntervalsRecordingFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
intervalHandler = new Handler();
|
||||
stackModeListView = IntervalStatisticsAdapter.StackMode.STACK_FROM_BOTTOM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
intervalHandler.post(intervalRunner);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
intervalHandler.removeCallbacks(intervalRunner);
|
||||
}
|
||||
|
||||
private void updateIntervals() {
|
||||
loadIntervals();
|
||||
}
|
||||
}
|
||||
}
|
||||