Compare commits

...

188 Commits

Author SHA1 Message Date
Dennis Guse 75b8f3b9cf Release: v3.14.1 2021-01-03 13:25:19 +01:00
Jaroslav Lichtblau 9462221a28 Translated using Weblate (Czech)
Currently translated at 100.0% (516 of 516 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/cs/
2020-12-31 12:34:58 +01:00
FTno af82eae8a4 Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.4% (511 of 514 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2020-12-31 12:34:58 +01:00
Román Martínez 1c4946e85b Move RuntimeException to ImportParseExceptio so app is not crashes and import process go on. Fixes #557. 2020-12-31 12:34:38 +01:00
Román Martínez fd1b7b8af9 Added opentracks:gain and opentracks:loss information in GPX files. Fixes #555. 2020-12-26 20:30:10 +01:00
Dennis Guse eb02c4c469 Marker images can now be shared from the list directly via the context menu (like tracks).
Fixes #495.
2020-12-25 18:36:41 +01:00
Román Martínez 4714171e21 Added elevation loss via barometer (like elevation gain). Fixes #547. 2020-12-25 18:19:01 +01:00
Dennis Guse 4bff376442 pt-BR: plural vs lint. 2020-12-20 00:17:00 +01:00
Dennis Guse c14413cbe7 CodeQL action: removed now unecessary step. 2020-12-20 00:03:42 +01:00
Vinicius 778fb4b346 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (507 of 507 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pt_BR/
2020-12-19 23:16:02 +01:00
Xosé M 130552f0eb Translated using Weblate (Galician)
Currently translated at 100.0% (507 of 507 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2020-12-19 23:16:02 +01:00
FTno 41eca663e8 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (507 of 507 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2020-12-19 23:16:02 +01:00
FTno 6c79702075 Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.0% (502 of 507 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2020-12-19 23:16:02 +01:00
FTno 6d109a1c5a Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.0% (497 of 507 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2020-12-19 23:16:02 +01:00
FTno b71904a3bd Translated using Weblate (Norwegian Bokmål)
Currently translated at 97.4% (494 of 507 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2020-12-19 23:16:01 +01:00
bortox ef28784682 Translated using Weblate (Italian)
Currently translated at 100.0% (507 of 507 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/it/
2020-12-19 23:16:01 +01:00
FTno 05b7242191 Translated using Weblate (Norwegian Bokmål)
Currently translated at 93.4% (474 of 507 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2020-12-19 23:16:01 +01:00
Xosé M 94c69f92a6 Translated using Weblate (Galician)
Currently translated at 100.0% (506 of 506 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/gl/
2020-12-19 23:16:01 +01:00
mitakjakub 8f12ba3d48 Translated using Weblate (Polish)
Currently translated at 86.1% (436 of 506 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/pl/
2020-12-19 23:16:01 +01:00
Román Martínez 190a572951 Translated using Weblate (Spanish)
Currently translated at 100.0% (506 of 506 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/es/
2020-12-19 23:16:01 +01:00
FTno 7ae8809101 Translated using Weblate (Norwegian Bokmål)
Currently translated at 93.6% (474 of 506 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://weblate.bubu1.eu/projects/OpenTracks/opentracks/nb_NO/
2020-12-19 23:16:01 +01:00
Dennis Guse 19a7ef3217 Activity icon now properly tint'ed in detail activity. 2020-12-19 18:29:34 +01:00
Dennis Guse f261e8ab9d Cleanup. 2020-12-17 23:16:09 +01:00
Dennis Guse a41160229d Implement basic EspressoUI test. 2020-12-17 23:16:09 +01:00
Dennis Guse 2fa8b998f9 Code cleanup using AndroidStudio's analyze feature. 2020-12-17 19:59:53 +01:00
Román Martínez 99847dbed9 Just tiny changes. 2020-12-17 19:15:35 +01:00
Román Martínez 483736637e Refactoring MarkerEditActivity. Logic has been moved to a ViewModel. 2020-12-17 19:15:35 +01:00
Dennis Guse e133743547 Actionbar: icon provides back functionality (except in TrackListActivity). 2020-12-17 18:56:42 +01:00
Dennis Guse eb2c2b4d78 As of ADT 14 as well as Gradle Plugin 5.0 those are not final anymore.
Fixes #544.

Missed in 1862e38a20
2020-12-15 23:36:03 +01:00
Dennis Guse 1409537e56 AggregatedStatistics: fixed ViewHolder pattern. 2020-12-15 22:53:52 +01:00
Dennis Guse 76c3699518 Intervals: list height should match parent. 2020-12-15 22:46:01 +01:00
Dennis Guse 5675dc800d Aggregated stats: ordered by number number of tracks per category. 2020-12-13 22:44:44 +01:00
Dennis Guse e1e7c24d9f Aggregated stats: icon now primary color, value centered. 2020-12-13 18:40:06 +01:00
Dennis Guse 2733071a4d Aggregated stats: simplified layout. 2020-12-13 18:14:39 +01:00
Dennis Guse 2255624235 Cleanup: rename waypoint to marker (tests). 2020-12-13 18:05:08 +01:00
Dennis Guse 55fa6d2d79 Code cleanup using AndroidStudio. 2020-12-13 18:05:08 +01:00
Dennis Guse 96371336f4 MarkerDetailFragment: load markerId once in onCreate(). 2020-12-13 17:12:03 +01:00
Dennis Guse 1862e38a20 Compare resourceIds via if rather than switch.
As of ADT 14 as well as Gradle Plugin 5.0 those are not final anymore.
Fixes #544.
2020-12-12 22:14:51 +01:00
Dennis Guse 397e056f6f Release: v3.14.0 2020-12-10 22:52:02 +01:00
Dennis Guse eb79f2277a ChartView let's parent handle TouchEvents if not zoomed. 2020-12-06 22:35:44 +01:00
Román Martínez ae8833d21b ViewPager migrate to ViewPager2. Fixes #522.
Swiping between tabs work. Fixes #105.
2020-12-06 22:35:44 +01:00
Dennis Guse e8017a4e9d Bugfix: In preferences, the Bluetooth search icon was only animated after opening the dialog a 2nd time. 2020-12-06 00:09:24 +01:00
Dennis Guse cea180f871 Code cleanup using AndroidStudio's Analyze feature. 2020-12-05 23:22:15 +01:00
Dennis Guse be1b6ece9d IntervalsFragment: generate UI values from strings.xml (i18n). 2020-12-05 23:11:29 +01:00
Vanous 0592863d3c add Huami devices into Readme Tested Sensors 2020-12-05 15:42:09 +01:00
Dennis Guse 18bc100d5a BLE sensors: Miband3's is now considered a heart rate sensor during discovery.
Some BLE devices do not announce all there supported services in the BLE advertisement packages.
Android's ScanFilter decides based upon this information only.
When connecting to such a device (and trigger service discovery), the service can still be used.

Fixes #447.
2020-12-05 12:36:57 +01:00
Peter Storch 4476a15558 throw Exception when no doc.kml file found in kmz 2020-11-29 13:56:22 +01:00
Peter Storch 215c28be85 fix #537 ignore waypoint markers with invalid
coordinates on import.

I've also added a unit test with such an export file.
2020-11-29 13:56:22 +01:00
Dennis Guse a429fbe4df Interval list: RTL and localized formatting (only default strings.xml). 2020-11-28 15:05:49 +01:00
Dennis Guse 6b470c7fdc Intervals: change label from elevation to gain. 2020-11-28 15:05:49 +01:00
Dennis Guse 6b690f38ad Charts: text size is resolved from styles.
Fixes #524.
2020-11-28 15:05:49 +01:00
Dennis Guse f138826d60 Text in charts now scales using Android's global settings.
Fixes #524.
2020-11-28 15:05:49 +01:00
Dennis Guse 14ac29095d Removed unused theme. 2020-11-28 15:05:49 +01:00
Dennis Guse a3f4de7d18 Interval list: table header in accent color. 2020-11-28 15:05:49 +01:00
Dennis Guse 6392e88dc7 Enable RTL. 2020-11-28 15:05:49 +01:00
FTno 1e913d6bb5 Update strings.xml
Typos in source strings - Issue #530
2020-11-26 07:35:34 +01:00
dependabot[bot] ced8454f59 Bump mockito-android from 3.6.0 to 3.6.28
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.6.0 to 3.6.28.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.6.0...v3.6.28)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-25 12:15:57 +01:00
Román Martínez 974a5afeb1 Only clean if parse error is out of <trk> 2020-11-25 06:39:07 +01:00
Román Martínez 9c9bdde6b9 Import files by track in memory (avoid clean track when error). Fixes #473. 2020-11-25 06:39:07 +01:00
Dennis Guse ca2bc6426c Release: v3.13.2 2020-11-22 20:21:50 +01:00
Dennis Guse 81097aac40 Bugfix: crash in landscape mode in TrackController due to wrong background. 2020-11-22 20:21:11 +01:00
Dennis Guse a74b5fc743 Cleanup. 2020-11-21 18:42:32 +01:00
Dennis Guse e996ad0a0c Release: v3.13.1 2020-11-21 10:37:25 +01:00
Dennis Guse 0f726958e0 Revert "Shrinking all APK builds without obfuscation. Fixes #433."
Introduced #523.

This reverts commit 34834120
2020-11-21 10:34:15 +01:00
Peter Storch 3a84520819 removed duplicate export 2020-11-20 21:56:39 +01:00
Dennis Guse 8328069b9d README: add human tech badge :)
Fixes #521.
2020-11-20 21:51:45 +01:00
Peter Storch b2df79caf2 fix #516 post workout export fails silently (#520)
Change the post workout export to use the ExportService (JobIntentService) and directing the user to the SettingsActivity in case the export failed to check the export directory.
2020-11-20 20:59:52 +01:00
Dennis Guse 6bcb8db40c Release: v3.13.0 2020-11-17 22:21:24 +01:00
Román Martínez 974baaf611 Marker Bugfixes: markers deletion didn't work and track import didn't include marker's photos. Fixes #514.
Marker edition bugfixed: orphan photos are deleted.
2020-11-17 22:02:26 +01:00
Dennis Guse fd77e77bb9 TrackController: readded stop button's pressed state.
Removed in 18546fd3cc
2020-11-17 21:49:21 +01:00
Dennis Guse 1e8beff015 TrackController: refactor button (delayed action) UI and use handler. 2020-11-17 21:43:12 +01:00
Dennis Guse 96bc347374 Activity icon uses now an image instead of a spinner (which was only used as an icon).
Fixed #503.
2020-11-17 21:43:12 +01:00
dependabot[bot] 8f144d4a97 Bump desugar_jdk_libs from 1.1.0 to 1.1.1
Bumps [desugar_jdk_libs](https://github.com/google/desugar_jdk_libs) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/google/desugar_jdk_libs/releases)
- [Changelog](https://github.com/google/desugar_jdk_libs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/desugar_jdk_libs/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-13 07:26:29 +01:00
Dennis Guse 3c3641376d Added power meter infos to READMEs. 2020-11-12 17:12:44 +01:00
Dennis Guse 35f4427aec Merge pull request #444 from OpenTracksApp/cyclingPower
Bluetooth Sensor: add cycling powermeter.
2020-11-12 12:00:13 +01:00
Dennis Guse 5a0cb10838 Bugfix: cadence and speed now can compare when data is not set. 2020-11-11 21:25:56 +01:00
Dennis Guse bf92851c97 Show sensor data's unit. 2020-11-11 21:08:39 +01:00
Dennis Guse 55c45b2050 Bugfix: SensorDataCycling.toString() should use attributes instead of their getters. 2020-11-11 21:08:39 +01:00
Román Martínez d01715444f Adaptive recording layout for sensors (heart rate, cycling cadence and cycling power). 2020-11-11 21:08:39 +01:00
Dennis Guse 634a7a4321 Bluetooth Sensor: add cycling powermeter. 2020-11-11 21:08:39 +01:00
Dennis Guse e5911a79ab Translation: Norwegian used wrong language code.
Introduced in 56aab12110
Fixes #515.
2020-11-11 20:07:59 +01:00
dependabot[bot] a78b3eb66d Bump gradle from 4.1.0 to 4.1.1
Bumps gradle from 4.1.0 to 4.1.1.

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-11 07:53:54 +01:00
Román Martínez 28890abd47 Bugfix: view changed from wrong thread. Fixes #509. 2020-11-09 20:19:08 +01:00
Dennis Guse 0a466d240b Wrap searchQuery in SQLite wildcards. 2020-11-09 18:56:25 +01:00
Dennis Guse 9ef312fc93 Search is now handled by TrackListActivity and MarkerListActivity.
* data is loaded via cursor,
* removed custom ranking (handled in database),
* search text is wrapped with wildcards, and
* search view can be closed by back button.

Fixes #492.
2020-11-09 18:56:25 +01:00
Dennis Guse 56aab12110 Noway's translation was included twice. 2020-11-09 18:55:38 +01:00
Dennis Guse c394448273 Removed Transifex configuration. 2020-11-09 18:41:38 +01:00
Dennis Guse 95671f4927 Translation is now on Weblate. 2020-11-09 18:27:38 +01:00
Dennis Guse 348341204e Shrinking all APK builds without obfuscation.
Fixes #433.
2020-11-09 12:09:37 +01:00
Román Martínez 82f5c127ee Bugfix: NPE in import. Fixes #506. 2020-11-08 14:33:31 +01:00
Dennis Guse dd490f528e Cursors must be closed. 2020-11-08 00:37:01 +01:00
Christoph Wurst 27731319f0 Fix internet access typo in readme
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
2020-11-07 10:05:41 +01:00
Christoph Wurst 86bfe6d0ce Fix formatting of md lists, remove trailing whitespaces
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
2020-11-07 09:21:55 +01:00
Christoph Wurst ea8bbb0b36 Update list of tested sensors with Polar H10 and Garmin SPD/CAD
Adds three tested sensors
* "Polar H10" heart rate sensor
* "Garmin Cadence Sensor 2" cadence sensor
* "Garmin Speed Sensor 2" speed sensor

Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
2020-11-07 09:21:55 +01:00
dependabot[bot] c02fdd3bba Bump desugar_jdk_libs from 1.0.10 to 1.1.0
Bumps [desugar_jdk_libs](https://github.com/google/desugar_jdk_libs) from 1.0.10 to 1.1.0.
- [Release notes](https://github.com/google/desugar_jdk_libs/releases)
- [Changelog](https://github.com/google/desugar_jdk_libs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/desugar_jdk_libs/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-04 07:46:08 +01:00
Dennis Guse d16cbe04a5 Release: v3.12.0 2020-11-03 20:11:21 +01:00
Peter Storch 90a8fc898a fix #491 crash export on android 11 2020-11-03 19:57:19 +01:00
Dennis Guse 8d141b1324 TrackListActivity now loads marker count via join rather than a 2nd select.
Fixes #328.
2020-11-01 21:53:11 +01:00
Peter Storch 43e0f33776 optimize multiple track deletion
After #480 got implemented, we can now delete multiple tracks in one go and only cause DB vacuum once.
2020-11-01 21:11:58 +01:00
Román Martínez e4270b983a ExportActivity bugfix: setContentView was missing. 2020-11-01 13:21:53 +01:00
Dennis Guse 0209a90e01 Code cleanup. 2020-11-01 08:38:47 +01:00
Dennis Guse 0f92a37146 Use ViewBinding for activities. 2020-11-01 08:20:26 +01:00
Dennis Guse 9cebb1013c Implement ViewBindings for Fragments. 2020-10-31 22:21:49 +01:00
Dennis Guse 6ac0db7238 Move handling of sensor removal (from settings) in BluetoothConnectionManager.BluetoothConnectionManager. 2020-10-31 22:21:49 +01:00
Dennis Guse 3c1ab01df6 SensorData uses null internally instead of NaN. 2020-10-31 22:21:49 +01:00
Dennis Guse b6850eb46f BluetoothSensorManager: on initiate connect to sensor, report address to be shown on UI.
Fixes #64.
2020-10-31 22:21:49 +01:00
Dennis Guse 08b9cb31fa StatisticsRecordingFragment does not need to check GPS accuracy. 2020-10-31 21:38:57 +01:00
Dennis Guse 9541a92657 Small cleanups and improvements code. 2020-10-31 20:39:37 +01:00
Román Martínez acc2627bde Activity with progress and summary for import all option. Fixes #471.
ErrorListDialog shared by ExportActivity and ImportActivity.
2020-10-31 20:39:37 +01:00
Dennis Guse 48c3695b5d Database enforces foreign key constraints incl. cascading deletes. (#480)
* Database enforces foreign key constraints incl. cascading deletes.
Fixes #317.
2020-10-30 21:26:13 +01:00
Dennis Guse 2b280f050c SharedContentProvider now uses parent ContentProvider directly instead of ContentProviderUtils. 2020-10-30 21:25:41 +01:00
dependabot[bot] 68f34d9682 Bump constraint-layout from 2.0.3 to 2.0.4
Bumps constraint-layout from 2.0.3 to 2.0.4.

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-30 09:07:44 +01:00
Dennis Guse f56820e132 Documentation: added TrackPoint descriptions regarding track segments. 2020-10-29 19:10:39 +01:00
dependabot[bot] 0f5d72f098 Bump constraint-layout from 2.0.2 to 2.0.3
Bumps constraint-layout from 2.0.2 to 2.0.3.

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-28 23:08:14 +01:00
dependabot[bot] accbc5a8ac Bump mockito-android from 3.5.15 to 3.6.0
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.5.15 to 3.6.0.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.5.15...v3.6.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-27 08:12:20 +01:00
Dennis Guse 0bc9f6a042 Update README_TESTED_SENSORS.md
Fixes #468.
2020-10-26 18:24:42 +01:00
Peter Storch bbb91fedc5 fix #470 Unable to parse kml in kmz 2020-10-22 19:22:46 +02:00
Peter Storch 433ab01ece Track.Id.toString returns string representation of id 2020-10-22 19:17:53 +02:00
Peter Storch 88e1a95da7 fix NPE introduced in #466 2020-10-22 19:08:52 +02:00
Román Martínez d4483819eb ImportActivity intent filters. (#466)
* ImportActivity intent filters.

* add intent_filter to work with Locus and OsmAnd

* extent intent filters

Co-authored-by: Peter Storch <peter.storch@gmail.com>
2020-10-21 22:52:39 +02:00
Román 917b8f4056 Edit texts: select all on focus and capitalize by words/sentences. Fixes #461. 2020-10-20 21:31:43 +02:00
Román 70c1379778 Export All: ask to the user when conflicts (track already exported in the selected directory) and show a summary. Fixes #451. 2020-10-20 12:07:58 +02:00
dependabot[bot] 5a60f40308 Bump mockito-android from 3.5.13 to 3.5.15
Bumps [mockito-android](https://github.com/mockito/mockito) from 3.5.13 to 3.5.15.
- [Release notes](https://github.com/mockito/mockito/releases)
- [Commits](https://github.com/mockito/mockito/compare/v3.5.13...v3.5.15)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-20 08:23:36 +02:00
Peter Storch df9589697a idea config files with android studio upgrade 2020-10-19 22:42:53 +02:00
Dennis Guse 27443cc716 Updated translations. 2020-10-17 12:45:48 +02:00
Dennis Guse 654be751a7 Theming: day/night mode is defaults to system. 2020-10-16 20:08:12 +02:00
Dennis Guse 3a8f3e46c7 Refactor post-workout export. 2020-10-16 12:23:39 +02:00
thrillfall b270c45691 - add default export directory
- add instant post workout track export
2020-10-16 12:23:39 +02:00
dependabot[bot] 257d8c1276 Bump gradle from 4.0.2 to 4.1.0
Bumps gradle from 4.0.2 to 4.1.0.

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-13 21:41:15 +02:00
Dennis Guse 8521b98f4b Bitcoin addresses are lower case (Bech32). 2020-10-13 21:24:59 +02:00
Dennis Guse eaf655f6e7 Upgrade gradle wrapper to 6.5 2020-10-13 18:15:47 +02:00
Dennis Guse 24c7f3fd7a Add option to donate via Bitcoin. 2020-10-13 18:07:28 +02:00
Román a4bab656e4 New screenshots. 2020-10-11 13:57:39 +02:00
Román 679c1d7e3e Intervals: TrackDataHub instead of ContentProvider. Fixes #453. 2020-10-11 12:12:44 +02:00
Román 0036934fb3 Intervals: added elevation gain and only shows speed or pace, depending on settings. 2020-10-11 12:12:44 +02:00
Román c508747203 Bugfix: import gpx file added heart rate like a speed because a break missing. 2020-10-08 12:39:31 +02:00
dependabot[bot] 0f4c0079d7 Bump gradle from 4.0.1 to 4.0.2
Bumps gradle from 4.0.1 to 4.0.2.

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-07 12:55:35 +02:00
dependabot[bot] 9e6bb1c511 Bump constraint-layout from 2.0.1 to 2.0.2
Bumps constraint-layout from 2.0.1 to 2.0.2.

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-18 08:07:00 +02:00
Dennis Guse 27386bb4f0 Localization: update via Transifex CLI. 2020-09-17 23:44:37 +02:00
Dennis Guse c6c07f03bf Localization: add configuration for Transifex CLI. 2020-09-17 23:43:28 +02:00
406 changed files with 9130 additions and 6979 deletions
-3
View File
@@ -19,9 +19,6 @@ jobs:
with:
fetch-depth: 2
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
+21
View File
@@ -1,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 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" 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">
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-194
View File
@@ -1,194 +0,0 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+49 -26
View File
@@ -2,77 +2,100 @@
_OpenTracks_ is a sport tracking application that completely respects your privacy.
[![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech)
<table>
<tr>
<th>Free</th>
<th>Paid (same as free)</th>
<th>Donations</th>
<th>Translations</th>
</tr>
<tr>
<td>
<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)
+1 -1
View File
@@ -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/).
+26 -2
View File
@@ -1,20 +1,44 @@
# Overview of tested Bluetooth LE sensors
## HRS: Heartrate Service
* Decathlon|Geonaute|Kalenji Dual HRM Belt
* moofit heart rate monitor
* Polar H7
* Polar H10
* Polar OH1
Has updatable firmware that requires a user account at polar.com
* Wahoo Tickr (Model: WFBTHR02)
* Mi Band 3, Amazfit Band 5, Amazfit Bip. These devices are manufactured by Huami, and other Huami devices will very likely work. Theoretically, this sensor must be enabled, but testing reveals that it works even without enabling. For details on how to enable see [Gadgetbridge wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Huami-Heartrate-measurement#bluetooth-heart-rate-sensor)
## CSCP: Cycling Cadence and Speed Service
Please note that the according to the specification these sensors _may_ provide speed as well as cadence.
However, often only one value is provided.
### Speed
* Garmin Speed Sensor 2
* Wahoo Speed (Model: WFRPMSPD)
### Cadence
### 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
+1 -1
View File
@@ -27,7 +27,7 @@ cp "$CHANGELOG_DIR/next_release.txt" "$CHANGELOG_FILE"
sed -i "s/(versionName)/$VERSIONNAME/" "$CHANGELOG_FILE"
echo -e "\nGit commit since last release\n------------"
git log $(git describe --tags --abbrev=0)..HEAD --no-merges --oneline
git log "$(git describe --tags --abbrev=0)..HEAD" --no-merges --oneline
echo -e "\nPlease edit the changelog for the new release"
gedit -w "$CHANGELOG_FILE"
+12 -7
View File
@@ -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,17 +45,21 @@ android {
compileSdkVersion 30
buildFeatures {
viewBinding = true
}
defaultConfig {
applicationId "de.dennisguse.opentracks"
versionCode 3611
versionName "v3.10.0"
versionCode 3799
versionName "v3.14.1"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
minSdkVersion 21
targetSdkVersion 29
targetSdkVersion 30
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -82,17 +86,18 @@ android {
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-alpha02'
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.28'
}
+9 -9
View File
@@ -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
+2 -1
View File
@@ -4,7 +4,8 @@ LocationManager --> TrackRecordingService
ElevationSumManager --> TrackRecordingService
TrackRecordingService --> TrackRecordingServiceConnection
TrackRecordingService --> Database
TrackRecordingService --> CustomContentProvider
CustomContentProvider --> Database
Database --> CustomContentProvider
CustomContentProvider --> TrackDataHub
+33 -3
View File
@@ -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.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
@@ -0,0 +1,10 @@
v3.14.0: OpenTracks
Changes:
- Properly support for right-to-left
- Import: faster cleanup after import failure
- Text in charts scales according to Android settings
- Heart rate: support for Huami devices (requires Gadgetbridge)
Developer:
- Import is now handled in-memory
@@ -0,0 +1,14 @@
v3.14.1: OpenTracks
Changes:
- Added elevation loss (via barometer)
- Updated translations (cz, gl, it, nb, pl, pt, sp)
- Markers can be shared from list
- Actionbar icon (top left) to go back
-
Bugfix:
- Potential crash on import
Developer:
- Implemented basic EspressoUI test
- Reference resourceIds via if rather than switch/case
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
-2
View File
@@ -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
Vendored
+103 -104
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,168 @@
package de.dennisguse.opentracks;
import android.view.MotionEvent;
import android.view.View;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.action.MotionEvents;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import com.google.android.material.tabs.TabLayout;
import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class EspressoUITest {
@Rule
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
@Rule
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant("android.permission.ACCESS_FINE_LOCATION");
@LargeTest
@Test
public void record_pause_resume_stop() {
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
trackControllerRecordButton.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_controller_stop));
// wait; stay recording
trackControllerRecordButton.perform(waitFor(5000));
//pause
trackControllerRecordButton.perform(veryLongTouch(1600));
// wait; stay paused
trackControllerRecordButton.perform(waitFor(1000));
// resume
trackControllerRecordButton.perform(click());
// wait; stay recording
trackControllerRecordButton.perform(waitFor(1000));
// stop;
trackControllerStopButton.perform(veryLongTouch(1600));
}
}
@LargeTest
@Test
public void record_move_through_tabs() {
{
// TrackListActivity: start recording
ViewInteraction trackControllerRecordButton = onView(withId(R.id.track_controller_record));
trackControllerRecordButton.perform(click());
}
{
// TrackRecordingActivity
ViewInteraction tabLayout = onView(withId(R.id.track_detail_activity_tablayout));
ViewInteraction trackControllerStopButton = onView(withId(R.id.track_controller_stop));
tabLayout.perform(selectTabAtIndex(1));
tabLayout.perform(waitFor(1000));
tabLayout.perform(selectTabAtIndex(2));
tabLayout.perform(waitFor(1000));
tabLayout.perform(selectTabAtIndex(3));
tabLayout.perform(waitFor(1000));
tabLayout.perform(selectTabAtIndex(0));
tabLayout.perform(waitFor(1000));
// stop
trackControllerStopButton.perform(veryLongTouch(1600));
}
}
private static ViewAction veryLongTouch(final int duration_ms) {
return new ViewAction() {
@Override
public String getDescription() {
return "Perform long touch.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
// Get view absolute position
int[] location = new int[2];
view.getLocationOnScreen(location);
// Offset coordinates by view position
float[] coordinates = new float[]{location[0] + 1, location[1] + 1};
// Send down event, pause, and send up
MotionEvent down = MotionEvents.sendDown(uiController, coordinates, new float[]{1f, 1f}).down;
uiController.loopMainThreadForAtLeast(duration_ms);
MotionEvents.sendUp(uiController, down, coordinates);
}
};
}
private static ViewAction waitFor(final long duration_ms) {
return new ViewAction() {
@Override
public String getDescription() {
return "Wait for milliseconds.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, final View view) {
uiController.loopMainThreadForAtLeast(duration_ms);
}
};
}
private static ViewAction selectTabAtIndex(final int index) {
return new ViewAction() {
@Override
public String getDescription() {
return "Selecting tab.";
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public void perform(UiController uiController, View view) {
TabLayout tabLayout = (TabLayout) view;
tabLayout.getTabAt(index).select();
}
};
}
}
@@ -48,7 +48,9 @@ public class ChartValueSeriesTest {
R.string.description_elevation_metric,
R.string.description_elevation_imperial,
R.color.chart_elevation_fill,
R.color.chart_elevation_border) {
R.color.chart_elevation_border,
15,
18) {
@Override
double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getElevation();
@@ -64,6 +64,7 @@ public class DescriptionGeneratorTest {
stats.setMaxElevation(550);
stats.setMinElevation(-500);
stats.setTotalElevationGain(6000);
stats.setTotalElevationLoss(6000);
stats.setStartTime_ms(START_TIME);
track.setTrackStatistics(stats);
track.setCategory("hiking");
@@ -84,6 +85,7 @@ public class DescriptionGeneratorTest {
+ "Max elevation: 550 m (1804 ft)<br>"
+ "Min elevation: -500 m (-1640 ft)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Elevation loss: 6000 m (19685 ft)<br>"
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "<br>";
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
@@ -100,6 +102,7 @@ public class DescriptionGeneratorTest {
stats.setMaxElevation(Double.POSITIVE_INFINITY);
stats.setMinElevation(Double.NEGATIVE_INFINITY);
stats.setTotalElevationGain(6000);
stats.setTotalElevationLoss(6000);
stats.setStartTime_ms(START_TIME);
track.setTrackStatistics(stats);
track.setCategory("hiking");
@@ -118,6 +121,7 @@ public class DescriptionGeneratorTest {
+ "Average moving pace: 0:15 min/km (0:24 min/mi)<br>"
+ "Fastest pace: 0:10 min/km (0:16 min/mi)<br>"
+ "Elevation gain: 6000 m (19685 ft)<br>"
+ "Elevation loss: 6000 m (19685 ft)<br>"
+ "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "<br>";
assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
@@ -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,8 @@ public class TestDataUtil {
public static final double INITIAL_LATITUDE = 37.0;
public static final double INITIAL_LONGITUDE = -57.0;
public static final double ALTITUDE_INTERVAL = 2.5;
public static final float ELEVATION_GAIN = 3;
public static final float ELEVATION_LOSS = 3;
/**
* Create a track without any trackPoints.
@@ -65,12 +67,13 @@ public class TestDataUtil {
trackPoint.setAccuracy((float) i / 100.0f);
trackPoint.setAltitude(i * ALTITUDE_INTERVAL);
trackPoint.setTime(i + 1);
trackPoint.setSpeed(5f + (i / 10));
trackPoint.setSpeed(5f + (i / 10f));
trackPoint.setHeartRate_bpm(100f + i);
trackPoint.setCyclingCadence_rpm(300f + i);
trackPoint.setPower(400f + i);
trackPoint.setElevationGain(500f + i);
trackPoint.setElevationGain(ELEVATION_GAIN);
trackPoint.setElevationLoss(ELEVATION_LOSS);
return trackPoint;
}
@@ -86,19 +89,19 @@ public class TestDataUtil {
}
/**
* Creates a Waypoint with a photo.
* Creates a Marker with a photo.
*
* @param context The context.
* @param trackId The track id.
* @param location The location.
* @return the Waypoint created.
* @return the Marker created.
*/
public static Marker createWaypointWithPhoto(Context context, Track.Id trackId, Location location) throws IOException {
public static Marker createMarkerWithPhoto(Context context, Track.Id trackId, Location location) throws IOException {
File dstFile = new File(FileUtils.getImageUrl(context, trackId));
dstFile.createNewFile();
Uri photoUri = FileUtils.getUriForFile(context, dstFile);
String photoUrl = photoUri.toString();
return new Marker("Waypoint name", "Waypoint description", "Waypoint category", "", trackId, 0.0, 0, location, photoUrl);
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, 0.0, 0, location, photoUrl);
}
}
@@ -65,7 +65,7 @@ import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class CustomContentProviderUtilsTest {
private static final String NAME_PREFIX = "test name";
private static final String MOCK_DESC = "Mock Next Waypoint Desc!";
private static final String MOCK_DESC = "Mock Next Marker Desc!";
private static final String TEST_DESC = "Test Desc!";
private static final String TEST_DESC_NEW = "Test Desc new!";
private static final String TEST_NAME_NEW = "Test Name new!";
@@ -198,7 +198,7 @@ public class CustomContentProviderUtilsTest {
when(cursorMock.isNull(columnIndex)).thenReturn(false);
when(cursorMock.getString(columnIndex)).thenReturn(name);
Track track = contentProviderUtils.createTrack(cursorMock);
Track track = ContentProviderUtils.createTrack(cursorMock);
assertEquals(trackId, track.getId());
assertEquals(name, track.getName());
}
@@ -208,20 +208,20 @@ public class CustomContentProviderUtilsTest {
*/
@Test
public void testDeleteAllTracks() {
// Insert track, points and waypoint at first.
// Insert track, points and marker at first.
Track.Id trackId = new Track.Id(System.currentTimeMillis());
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
Marker waypoint = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
contentProviderUtils.insertMarker(waypoint);
Marker marker = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
contentProviderUtils.insertMarker(marker);
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
assertEquals(1, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(10, tracksPointsCursor.getCount());
Cursor waypointCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(1, waypointCursor.getCount());
Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(1, markerCursor.getCount());
// Delete all.
contentProviderUtils.deleteAllTracks(context);
// Check whether all have been deleted.
@@ -229,32 +229,32 @@ public class CustomContentProviderUtilsTest {
assertEquals(0, tracksCursor.getCount());
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(0, tracksPointsCursor.getCount());
waypointCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(0, waypointCursor.getCount());
markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(0, markerCursor.getCount());
}
/**
* Tests the method {@link ContentProviderUtils#deleteAllTracks(Context)}
*/
@Test
public void testDeleteAllTracks_withWaypointAndPhoto() throws IOException {
// Insert track, points and waypoint with photo at first.
public void testDeleteAllTracks_withMarkerAndPhoto() throws IOException {
// Insert track, points and marker with photo at first.
Track.Id trackId = new Track.Id(System.currentTimeMillis());
Track track = TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
Marker waypoint = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
contentProviderUtils.insertMarker(waypoint);
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
contentProviderUtils.insertMarker(marker);
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
assertEquals(1, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(10, tracksPointsCursor.getCount());
Cursor waypointCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(1, waypointCursor.getCount());
// Check waypoint has photo and it's in the external storage.
assertTrue(waypoint.hasPhoto());
Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(1, markerCursor.getCount());
// Check marker has photo and it's in the external storage.
assertTrue(marker.hasPhoto());
File dir = FileUtils.getPhotoDir(context, trackId);
assertTrue(dir.isDirectory());
assertEquals(1, dir.list().length);
@@ -266,8 +266,8 @@ public class CustomContentProviderUtilsTest {
assertEquals(0, tracksCursor.getCount());
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(0, tracksPointsCursor.getCount());
waypointCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(0, waypointCursor.getCount());
markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(0, markerCursor.getCount());
assertFalse(dir.exists());
}
@@ -287,17 +287,16 @@ public class CustomContentProviderUtilsTest {
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId2, 10);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId3, 10);
Marker waypoint = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId2));
waypoint.setTrackId(trackId1);
contentProviderUtils.insertMarker(waypoint);
Marker marker = new Marker(trackId1, contentProviderUtils.getLastValidTrackPoint(trackId2));
contentProviderUtils.insertMarker(marker);
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
assertEquals(3, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(20, tracksPointsCursor.getCount());
Cursor waypointCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(1, waypointCursor.getCount());
Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(1, markerCursor.getCount());
// Delete one track.
contentProviderUtils.deleteTrack(context, trackId1);
// Check whether all data of a track has been deleted.
@@ -305,15 +304,15 @@ public class CustomContentProviderUtilsTest {
assertEquals(2, tracksCursor.getCount());
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(20, tracksPointsCursor.getCount());
waypointCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(0, waypointCursor.getCount());
markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(0, markerCursor.getCount());
}
/**
* Tests the method {@link ContentProviderUtils#deleteTrack(Context, Track.Id)}.
*/
@Test
public void testDeleteTrack_withWaypointPhoto() throws IOException {
public void testDeleteTrack_withMarkerPhoto() throws IOException {
// Insert three tracks.
long random = System.currentTimeMillis();
Track.Id trackId1 = new Track.Id(random);
@@ -324,15 +323,15 @@ public class CustomContentProviderUtilsTest {
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId2, 10);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId3, 10);
// Insert a waypoint in tracks trackId and trackId + 1.
// Insert a marker in tracks trackId and trackId + 1.
TrackPoint trackPoint1 = contentProviderUtils.getLastValidTrackPoint(trackId1);
Marker waypoint1 = TestDataUtil.createWaypointWithPhoto(context, trackId1, trackPoint1.getLocation());
contentProviderUtils.insertMarker(waypoint1);
Marker marker1 = TestDataUtil.createMarkerWithPhoto(context, trackId1, trackPoint1.getLocation());
contentProviderUtils.insertMarker(marker1);
File dir1 = FileUtils.getPhotoDir(context, trackId1);
TrackPoint trackPoint2 = contentProviderUtils.getLastValidTrackPoint(trackId2);
Marker waypoint2 = TestDataUtil.createWaypointWithPhoto(context, trackId2, trackPoint2.getLocation());
contentProviderUtils.insertMarker(waypoint2);
Marker marker2 = TestDataUtil.createMarkerWithPhoto(context, trackId2, trackPoint2.getLocation());
contentProviderUtils.insertMarker(marker2);
File dir2 = FileUtils.getPhotoDir(context, trackId2);
// Check.
@@ -341,9 +340,9 @@ public class CustomContentProviderUtilsTest {
assertEquals(3, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(30, tracksPointsCursor.getCount());
Cursor waypointCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(2, waypointCursor.getCount());
assertTrue(waypoint1.hasPhoto());
Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(2, markerCursor.getCount());
assertTrue(marker1.hasPhoto());
assertTrue(dir1.isDirectory());
assertEquals(1, dir1.list().length);
assertTrue(dir1.exists());
@@ -357,8 +356,8 @@ public class CustomContentProviderUtilsTest {
assertEquals(2, tracksCursor.getCount());
tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(20, tracksPointsCursor.getCount());
waypointCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(1, waypointCursor.getCount());
markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(1, markerCursor.getCount());
assertFalse(dir1.exists());
assertTrue(dir2.exists());
}
@@ -381,16 +380,6 @@ public class CustomContentProviderUtilsTest {
assertEquals(trackId, allTracks.get(allTracks.size() - 1).getId());
}
/**
* Tests the method {@link ContentProviderUtils#getLastTrack()}
*/
@Test
public void testGetLastTrack() {
Track.Id trackId = new Track.Id(System.currentTimeMillis());
contentProviderUtils.insertTrack(TestDataUtil.createTrack(trackId));
assertEquals(trackId, contentProviderUtils.getLastTrack().getId());
}
/**
* Tests the method {@link ContentProviderUtils#getTrack(Track.Id)}
*/
@@ -442,7 +431,7 @@ public class CustomContentProviderUtilsTest {
* Tests the method {@link ContentProviderUtils#createContentValues(Marker)}.
*/
@Test
public void testCreateContentValues_waypoint() {
public void testCreateContentValues_marker() {
Track.Id trackId = new Track.Id(System.currentTimeMillis());
Pair<Track, TrackPoint[]> track = TestDataUtil.createTrack(trackId, 10);
@@ -463,16 +452,16 @@ public class CustomContentProviderUtilsTest {
track.first.setTrackStatistics(statistics);
contentProviderUtils.insertTrack(track.first);
Marker waypoint = new Marker(track.second[0]);
waypoint.setDescription(TEST_DESC);
contentProviderUtils.insertMarker(waypoint);
Marker marker = new Marker(trackId, track.second[0]);
marker.setDescription(TEST_DESC);
contentProviderUtils.insertMarker(marker);
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(contentResolverMock);
Marker.Id waypointId = new Marker.Id(System.currentTimeMillis());
waypoint.setId(waypointId);
ContentValues contentValues = contentProviderUtils.createContentValues(waypoint);
assertEquals(waypointId.getId(), contentValues.get(MarkerColumns._ID));
Marker.Id markerId = new Marker.Id(System.currentTimeMillis());
marker.setId(markerId);
ContentValues contentValues = contentProviderUtils.createContentValues(marker);
assertEquals(markerId.getId(), contentValues.get(MarkerColumns._ID));
assertEquals((int) (TestDataUtil.INITIAL_LONGITUDE * 1000000), contentValues.get(MarkerColumns.LONGITUDE));
assertEquals(TEST_DESC, contentValues.get(MarkerColumns.DESCRIPTION));
}
@@ -481,7 +470,7 @@ public class CustomContentProviderUtilsTest {
* Tests the method {@link ContentProviderUtils#createMarker(Cursor)}.
*/
@Test
public void testCreateWaypoint() {
public void testCreateMarker() {
int startColumnIndex = 1;
int columnIndex = startColumnIndex;
when(cursorMock.getColumnIndexOrThrow(MarkerColumns._ID)).thenReturn(columnIndex++);
@@ -505,81 +494,80 @@ public class CustomContentProviderUtilsTest {
long trackId = 11L;
when(cursorMock.getLong(columnIndex++)).thenReturn(trackId);
Marker waypoint = contentProviderUtils.createMarker(cursorMock);
assertEquals(id, waypoint.getId().getId());
assertEquals(name, waypoint.getName());
assertEquals(trackId, waypoint.getTrackId().getId());
Marker marker = contentProviderUtils.createMarker(cursorMock);
assertEquals(id, marker.getId().getId());
assertEquals(name, marker.getName());
assertEquals(trackId, marker.getTrackId().getId());
}
/**
* Tests the method
* {@link ContentProviderUtils#deleteMarker(Context, Marker.Id)}
* when there is only one waypoint in the track.
* when there is only one marker in the track.
*/
@Test
public void testDeleteWaypoint_onlyOneWayPoint() {
public void testDeleteMarker_onlyOneMarker() {
Track.Id trackId = new Track.Id(System.currentTimeMillis());
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
// Insert at first.
Marker waypoint1 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint1.setDescription(TEST_DESC);
waypoint1.setTrackId(trackId);
contentProviderUtils.insertMarker(waypoint1);
Marker marker1 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
marker1.setDescription(TEST_DESC);
contentProviderUtils.insertMarker(marker1);
// Check insert was done.
assertEquals(contentProviderUtils.getMarkerCount(trackId), 1);
// Get waypoint id that needs to delete.
Marker.Id waypoint1Id = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint1)));
// Get marker id that needs to delete.
Marker.Id marker1Id = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker1)));
// Delete
contentProviderUtils.deleteMarker(context, waypoint1Id);
contentProviderUtils.deleteMarker(context, marker1Id);
assertNull(contentProviderUtils.getMarker(waypoint1Id));
assertNull(contentProviderUtils.getMarker(marker1Id));
}
/**
* Tests the method
* {@link ContentProviderUtils#deleteMarker(Context, Marker.Id)}
* when there is only one waypoint in the track.
* when there is only one marker in the track.
*/
@Test
public void testDeleteWaypoint_onlyOneWayPointWithPhotoUrl() throws IOException {
public void testDeleteMarker_onlyOneMarkerWithPhotoUrl() throws IOException {
Track.Id trackId = new Track.Id(System.currentTimeMillis());
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
// Insert at first.
TrackPoint trackPoint = contentProviderUtils.getLastValidTrackPoint(trackId);
Marker waypoint1 = TestDataUtil.createWaypointWithPhoto(context, trackId, trackPoint.getLocation());
contentProviderUtils.insertMarker(waypoint1);
Marker marker1 = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
contentProviderUtils.insertMarker(marker1);
// Check insert was done.
assertEquals(contentProviderUtils.getMarkerCount(trackId), 1);
// Get waypoint id that needs to delete.
Marker.Id waypoint1Id = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint1)));
// Get marker id that needs to delete.
Marker.Id marker1Id = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker1)));
// Check waypoint has photo and it's in the external storage.
assertTrue(waypoint1.hasPhoto());
// Check marker has photo and it's in the external storage.
assertTrue(marker1.hasPhoto());
File dir = FileUtils.getPhotoDir(context, trackId);
assertTrue(dir.isDirectory());
assertEquals(1, dir.list().length);
assertTrue(dir.exists());
// Delete
contentProviderUtils.deleteMarker(context, waypoint1Id);
contentProviderUtils.deleteMarker(context, marker1Id);
// Check waypoint doesn't exists and photo folder was deleted.
assertNull(contentProviderUtils.getMarker(waypoint1Id));
// Check marker doesn't exists and photo folder was deleted.
assertNull(contentProviderUtils.getMarker(marker1Id));
assertFalse(dir.exists());
}
/**
* Tests the method {@link ContentProviderUtils#deleteMarker(Context, Marker.Id)} when there is more than one waypoint in the track.
* Tests the method {@link ContentProviderUtils#deleteMarker(Context, Marker.Id)} when there is more than one marker in the track.
*/
@Test
public void testDeleteWaypoint_hasNextWayPoint() {
public void testDeleteMarker_hasNextMarker() {
Track.Id trackId = new Track.Id(System.currentTimeMillis());
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
@@ -600,44 +588,38 @@ public class CustomContentProviderUtilsTest {
// TestDataUtil.insertTrackWithLocations(contentProviderUtils, track);
// Insert at first.
Marker waypoint1 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint1.setDescription(MOCK_DESC);
waypoint1.setTrackId(trackId);
Marker.Id waypoint1Id = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint1)));
Marker marker1 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
marker1.setDescription(MOCK_DESC);
Marker.Id marker1Id = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker1)));
Marker waypoint2 = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint2.setDescription(MOCK_DESC);
waypoint2.setTrackId(trackId);
Marker.Id waypoint2Id = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint2)));
Marker marker2 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
marker2.setDescription(MOCK_DESC);
Marker.Id marker2Id = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker2)));
// Delete
assertNotNull(contentProviderUtils.getMarker(waypoint1Id));
contentProviderUtils.deleteMarker(context, waypoint1Id);
assertNull(contentProviderUtils.getMarker(waypoint1Id));
assertNotNull(contentProviderUtils.getMarker(marker1Id));
contentProviderUtils.deleteMarker(context, marker1Id);
assertNull(contentProviderUtils.getMarker(marker1Id));
assertEquals(MOCK_DESC, contentProviderUtils.getMarker(waypoint2Id).getDescription());
assertEquals(MOCK_DESC, contentProviderUtils.getMarker(marker2Id).getDescription());
}
/**
* Tests the method {@link ContentProviderUtils#getNextMarkerNumber(Track.Id)}.
*/
@Test
public void testGetNextWaypointNumber() {
public void testGetNextMarkerNumber() {
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);
contentProviderUtils.insertMarker(waypoint1);
contentProviderUtils.insertMarker(waypoint2);
contentProviderUtils.insertMarker(waypoint3);
contentProviderUtils.insertMarker(waypoint4);
Marker marker1 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
Marker marker2 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
Marker marker3 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
Marker marker4 = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
contentProviderUtils.insertMarker(marker1);
contentProviderUtils.insertMarker(marker2);
contentProviderUtils.insertMarker(marker3);
contentProviderUtils.insertMarker(marker4);
assertEquals(4, contentProviderUtils.getNextMarkerNumber(trackId));
}
@@ -647,56 +629,53 @@ public class CustomContentProviderUtilsTest {
* {@link ContentProviderUtils#getMarker(Marker.Id)}.
*/
@Test
public void testInsertAndGetWaypoint() {
public void testInsertAndGetMarker() {
Track.Id trackId = new Track.Id(System.currentTimeMillis());
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
Marker waypoint = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint.setDescription(TEST_DESC);
waypoint.setTrackId(trackId);
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
Marker marker = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
marker.setDescription(TEST_DESC);
Marker.Id markerId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker)));
assertEquals(TEST_DESC, contentProviderUtils.getMarker(waypointId).getDescription());
assertEquals(TEST_DESC, contentProviderUtils.getMarker(markerId).getDescription());
}
/**
* Tests the method {@link ContentProviderUtils#updateMarker(Context, Marker)}.
*/
@Test
public void testUpdateWaypoint() {
public void testUpdateMarker() {
Track.Id trackId = new Track.Id(System.currentTimeMillis());
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
// Insert at first.
Marker waypoint = new Marker(contentProviderUtils.getLastValidTrackPoint(trackId));
waypoint.setDescription(TEST_DESC);
waypoint.setTrackId(trackId);
Marker.Id waypointId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(waypoint)));
Marker marker = new Marker(trackId, contentProviderUtils.getLastValidTrackPoint(trackId));
marker.setDescription(TEST_DESC);
Marker.Id markerId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker)));
// Update
waypoint = contentProviderUtils.getMarker(waypointId);
waypoint.setDescription(TEST_DESC_NEW);
contentProviderUtils.updateMarker(context, waypoint);
marker = contentProviderUtils.getMarker(markerId);
marker.setDescription(TEST_DESC_NEW);
contentProviderUtils.updateMarker(context, marker);
assertEquals(TEST_DESC_NEW, contentProviderUtils.getMarker(waypointId).getDescription());
assertEquals(TEST_DESC_NEW, contentProviderUtils.getMarker(markerId).getDescription());
}
/**
* Tests the method {@link ContentProviderUtils#updateMarker(Context, Marker)}.
*/
@Test
public void testUpdateWaypoint_withPhoto() throws IOException {
// tests after update waypoint with photo the photo remains in the storage.
public void testUpdateMarker_withPhoto() throws IOException {
// tests after update marker with photo the photo remains in the storage.
Track.Id trackId = new Track.Id(System.currentTimeMillis());
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
// Insert at first.
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)));
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
marker.setDescription(TEST_DESC);
Marker.Id markerId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker)));
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId.getId());
assertTrue(dir.exists());
@@ -704,14 +683,14 @@ public class CustomContentProviderUtilsTest {
assertEquals(1, dir.list().length);
// Update
waypoint = contentProviderUtils.getMarker(waypointId);
waypoint.setName(TEST_NAME_NEW);
waypoint.setDescription(TEST_DESC_NEW);
contentProviderUtils.updateMarker(context, waypoint);
marker = contentProviderUtils.getMarker(markerId);
marker.setName(TEST_NAME_NEW);
marker.setDescription(TEST_DESC_NEW);
contentProviderUtils.updateMarker(context, marker);
assertEquals(TEST_NAME_NEW, contentProviderUtils.getMarker(waypointId).getName());
assertEquals(TEST_DESC_NEW, contentProviderUtils.getMarker(waypointId).getDescription());
assertTrue(waypoint.hasPhoto());
assertEquals(TEST_NAME_NEW, contentProviderUtils.getMarker(markerId).getName());
assertEquals(TEST_DESC_NEW, contentProviderUtils.getMarker(markerId).getDescription());
assertTrue(marker.hasPhoto());
assertTrue(dir.exists());
assertTrue(dir.isDirectory());
assertEquals(1, dir.list().length);
@@ -721,18 +700,17 @@ public class CustomContentProviderUtilsTest {
* Tests the method {@link ContentProviderUtils#updateMarker(Context, Marker)}.
*/
@Test
public void testUpdateWaypoint_delPhotoAndDir() throws IOException {
// tests after update waypoint if user deletes the photo then file photo is deleted from the storage. Also empty directory is deleted.
public void testUpdateMarker_delPhotoAndDir() throws IOException {
// tests after update marker if user deletes the photo then file photo is deleted from the storage. Also empty directory is deleted.
Track.Id trackId = new Track.Id(System.currentTimeMillis());
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
// Insert at first.
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)));
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
marker.setDescription(TEST_DESC);
Marker.Id markerId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker)));
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId.getId());
assertTrue(dir.exists());
@@ -740,15 +718,15 @@ public class CustomContentProviderUtilsTest {
assertEquals(1, dir.list().length);
// Update
waypoint = contentProviderUtils.getMarker(waypointId);
waypoint.setName(TEST_NAME_NEW);
waypoint.setDescription(TEST_DESC_NEW);
waypoint.setPhotoUrl(null);
contentProviderUtils.updateMarker(context, waypoint);
marker = contentProviderUtils.getMarker(markerId);
marker.setName(TEST_NAME_NEW);
marker.setDescription(TEST_DESC_NEW);
marker.setPhotoUrl(null);
contentProviderUtils.updateMarker(context, marker);
assertEquals(TEST_NAME_NEW, contentProviderUtils.getMarker(waypointId).getName());
assertEquals(TEST_DESC_NEW, contentProviderUtils.getMarker(waypointId).getDescription());
assertFalse(waypoint.hasPhoto());
assertEquals(TEST_NAME_NEW, contentProviderUtils.getMarker(markerId).getName());
assertEquals(TEST_DESC_NEW, contentProviderUtils.getMarker(markerId).getDescription());
assertFalse(marker.hasPhoto());
assertFalse(dir.exists());
}
@@ -756,35 +734,33 @@ public class CustomContentProviderUtilsTest {
* Tests the method {@link ContentProviderUtils#updateMarker(Context, Marker)}.
*/
@Test
public void testUpdateWaypoint_delPhotoNotDir() throws IOException {
// tests after update waypoint if user deletes the photo then file photo is deleted from the storage. Directory remains if there are more photos from other waypoints.
public void testUpdateMarker_delPhotoNotDir() throws IOException {
// tests after update marker if user deletes the photo then file photo is deleted from the storage. Directory remains if there are more photos from other markers.
Track.Id trackId = new Track.Id(System.currentTimeMillis());
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId, 10);
// Insert two waypoints with photos.
// Insert two markers with photos.
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);
Marker marker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
marker.setDescription(TEST_DESC);
Marker otherMarker = TestDataUtil.createMarkerWithPhoto(context, trackId, trackPoint.getLocation());
otherMarker.setDescription(TEST_DESC);
Marker.Id markerId = new Marker.Id(ContentUris.parseId(contentProviderUtils.insertMarker(marker)));
contentProviderUtils.insertMarker(otherMarker);
File dir = new File(FileUtils.getPhotoDir(context), "" + trackId.getId());
assertTrue(dir.exists());
assertTrue(dir.isDirectory());
assertEquals(2, dir.list().length);
// Update one waypoint deleting photo.
waypoint = contentProviderUtils.getMarker(waypointId);
waypoint.setPhotoUrl(null);
contentProviderUtils.updateMarker(context, waypoint);
// Update one marker deleting photo.
marker = contentProviderUtils.getMarker(markerId);
marker.setPhotoUrl(null);
contentProviderUtils.updateMarker(context, marker);
assertEquals(TEST_DESC, contentProviderUtils.getMarker(waypointId).getDescription());
assertFalse(waypoint.hasPhoto());
assertEquals(TEST_DESC, contentProviderUtils.getMarker(markerId).getDescription());
assertFalse(marker.hasPhoto());
assertTrue(dir.exists());
assertTrue(dir.isDirectory());
assertEquals(1, dir.list().length);
@@ -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);
}
}
@@ -28,7 +28,6 @@ import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import de.dennisguse.opentracks.TrackStubUtils;
import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -14,7 +14,7 @@
* the License.
*/
package de.dennisguse.opentracks;
package de.dennisguse.opentracks.fragments;
import android.location.Location;
@@ -1,131 +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.io.file.importer;
import android.content.ContentUris;
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import java.text.SimpleDateFormat;
import java.time.ZoneOffset;
import java.util.Locale;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.when;
/**
* Abstract class for testing file track importers.
*
* @author Jimmy Shih.
*/
public abstract class AbstractTestFileTrackImporter {
static final String TRACK_NAME_0 = "blablub";
static final String TRACK_DESCRIPTION_0 = "s'Laebe isch koi Schlotzer";
static final double TRACK_LATITUDE = 48.768364;
static final double TRACK_LONGITUDE = 9.177886;
static final double TRACK_ELEVATION = 324.0;
static final String TRACK_TIME_0 = "2010-04-22T18:21:00Z";
static final String TRACK_TIME_1 = "2010-04-22T18:21:50.123Z";
static final String TRACK_TIME_2 = "2010-04-22T18:23:00.123Z";
static final String TRACK_TIME_3 = "2010-04-22T18:24:50.123Z";
static final SimpleDateFormat DATE_FORMAT_0 = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'", Locale.US);
static final SimpleDateFormat DATE_FORMAT_1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US);
static final long TRACK_POINT_ID_0 = 1;
static final long TRACK_POINT_ID_1 = 2;
static final long TRACK_POINT_ID_3 = 4;
private static final long WAYPOINT_ID_0 = 1;
private static final Uri WAYPOINT_ID_O_URI = ContentUris.appendId(MarkerColumns.CONTENT_URI.buildUpon(), WAYPOINT_ID_0).build();
protected final Context context = ApplicationProvider.getApplicationContext();
static {
// We can't omit the timezones in the test, otherwise it'll use the local timezone and fail depending on where the test runner is.
TimeZone utc = SimpleTimeZone.getTimeZone(ZoneOffset.UTC);
DATE_FORMAT_0.setTimeZone(utc);
DATE_FORMAT_1.setTimeZone(utc);
}
@Mock
public ContentProviderUtils contentProviderUtils;
TrackPoint createTrackPoint(int index, long time) {
Location trackPoint = new Location(LocationManager.GPS_PROVIDER);
trackPoint.setLatitude(TRACK_LATITUDE + index);
trackPoint.setLongitude(TRACK_LONGITUDE + index);
trackPoint.setAltitude(TRACK_ELEVATION + index);
trackPoint.setTime(time);
return new TrackPoint(trackPoint);
}
/**
* Expects the first track point to be added.
*
* @param trackPoint the trackPoint
* @param trackId the track id
* @param trackPointId the track point id
*/
protected void expectFirstTrackPoint(TrackPoint trackPoint, Track.Id trackId, long trackPointId) {
when(contentProviderUtils.bulkInsertTrackPoint(trackPoint != null ? (TrackPoint[]) any() : (TrackPoint[]) any(), eq(trackId))).thenReturn(1);
}
/**
* Expects the track to be updated.
*
* @param trackCaptor the track
* @param lastTrack true if it is the last track in the gpx
* @param trackId the track id
*/
protected void expectTrackUpdate(ArgumentCaptor<Track> trackCaptor, boolean lastTrack, Track.Id trackId) {
contentProviderUtils.updateTrack(trackCaptor.capture());
when(contentProviderUtils.insertMarker(any())).thenReturn(WAYPOINT_ID_O_URI);
if (lastTrack) {
// Return null to not add waypoints
when(contentProviderUtils.getTrack(trackId)).thenReturn(null);
}
}
protected void verifyTrack(Track track, String name, String description, long time) {
assertEquals(name, track.getName());
assertEquals(description, track.getDescription());
if (time != -1L) {
assertEquals(time, track.getTrackStatistics().getStartTime_ms());
}
}
}
@@ -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, false);
assertTrackpoints(false, false, false, false, false);
}
@LargeTest
@@ -168,11 +170,11 @@ public class ExportImportTest {
assertMarkers();
// 3. trackpoints
assertTrackpoints(true, true, true, true);
assertTrackpoints(true, 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, false);
assertTrackpoints(false, true, true, true, true);
}
@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, boolean verifyElevationGain) {
private void assertTrackpoints(boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyElevationGain, boolean verifyElevationLoss) {
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
assertEquals(trackPoints.size(), importedTrackPoints.size());
@@ -333,6 +339,9 @@ public class ExportImportTest {
if (verifyElevationGain) {
assertEquals(trackPoint.getElevationGain(), importedTrackPoint.getElevationGain(), 0.01);
}
if (verifyElevationLoss) {
assertEquals(trackPoint.getElevationLoss(), importedTrackPoint.getElevationLoss(), 0.01);
}
}
}
}
@@ -0,0 +1,93 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.InputStream;
import java.util.List;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.util.StringUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
/**
* Imports a legacy kml file with a statistics marker at the beginning
*/
@RunWith(JUnit4.class)
public class LegacyKmlImportTest {
private final Context context = ApplicationProvider.getApplicationContext();
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
private Track.Id importTrackId;
@After
public void tearDown() {
if (importTrackId != null) {
contentProviderUtils.deleteTrack(context, importTrackId);
}
}
@LargeTest
@Test
public void kml_legacy_track_import() {
// given
KmlFileTrackImporter trackImporter = new KmlFileTrackImporter(context);
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.legacy_track);
// when
// 1. import
importTrackId = trackImporter.importFile(inputStream);
// then
// 1. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("unknown", importedTrack.getCategory());
assertEquals("Test Track", importedTrack.getDescription());
assertEquals("2020-11-28 18:06", importedTrack.getName());
assertEquals("UNKNOWN", importedTrack.getIcon());
// 2. markers
assertEquals(0, contentProviderUtils.getMarkerCount(importTrackId));
// 3. trackpoints
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
assertEquals(8, importedTrackPoints.size()); // 6 trackpoints + pause and resume
// first 3 trackpoints
assertTrackpoint(importedTrackPoints.get(0), "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
assertTrackpoint(importedTrackPoints.get(1), "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
assertTrackpoint(importedTrackPoints.get(2), "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
// created pause trackpoint with time of previous valid trackpoint
assertTrackpoint(importedTrackPoints.get(3), "2020-11-28T17:06:47.888Z", 0.0, 100, 0.0);
// created resume trackpoint with time of next valid trackpoint
assertTrackpoint(importedTrackPoints.get(4), "2020-11-28T17:06:55.861Z", 0.0, 200, 0.0);
// last 3 trackpoints
assertTrackpoint(importedTrackPoints.get(5), "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
assertTrackpoint(importedTrackPoints.get(6), "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
assertTrackpoint(importedTrackPoints.get(7), "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
}
private void assertTrackpoint(final TrackPoint trackPoint, final String when, final double longitude, final double latitude, final double altitude) {
assertEquals(StringUtils.parseTime(when), trackPoint.getTime());
assertEquals(latitude, trackPoint.getLatitude(), 0.001d);
assertEquals(longitude, trackPoint.getLongitude(), 0.001d);
assertEquals(altitude, trackPoint.getAltitude(), 0.001d);
}
}
@@ -138,7 +138,7 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testRecording_noTracks() throws Exception {
public void testRecording_noTracks() throws TimeoutException {
// given
List<Track> tracks = contentProviderUtils.getTracks();
assertTrue(tracks.isEmpty());
@@ -156,7 +156,7 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testRecording_oldTracks() throws Exception {
public void testRecording_oldTracks() throws TimeoutException {
// given
createDummyTrack(trackId, -1L, false);
@@ -170,7 +170,7 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testRecording_serviceRestart_whileRecording() throws Exception {
public void testRecording_serviceRestart_whileRecording() throws TimeoutException {
// given
createDummyTrack(trackId, -1L, true);
@@ -183,7 +183,7 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testRecording_pauseAndResume() throws Exception {
public void testRecording_pauseAndResume() throws TimeoutException, InterruptedException {
// given
createDummyTrack(trackId, -1L, true);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
@@ -212,7 +212,7 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testRecording_resumeStoppedTrack() throws Exception {
public void testRecording_resumeStoppedTrack() throws TimeoutException, InterruptedException {
// given
createDummyTrack(trackId, -1L, true);
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
@@ -238,7 +238,7 @@ public class TrackRecordingServiceTest {
@Ignore("Sometimes fails on CI.")
@MediumTest
@Test
public void testRecording_orphanedRecordingTrack() throws Exception {
public void testRecording_orphanedRecordingTrack() throws TimeoutException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
@@ -254,7 +254,7 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testStartNewTrack_alreadyRecording() throws Exception {
public void testStartNewTrack_alreadyRecording() throws TimeoutException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
@@ -274,7 +274,7 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testEndCurrentTrack_noRecording() throws Exception {
public void testEndCurrentTrack_noRecording() throws TimeoutException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
assertFalse(service.isRecording());
@@ -290,21 +290,21 @@ public class TrackRecordingServiceTest {
@MediumTest
@Test
public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
public void testInsertWaypointMarker_noRecordingTrack() throws TimeoutException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
assertFalse(service.isRecording());
// when
Marker.Id waypointId = service.insertMarker(null, null, null, null);
Marker.Id markerId = service.insertMarker(null, null, null, null);
// then
assertNull(waypointId);
assertNull(markerId);
}
@MediumTest
@Test
public void testInsertWaypointMarker_validWaypoint() throws Exception {
public void testInsertWaypointMarker_validWaypoint() throws TimeoutException, InterruptedException {
// given
TrackRecordingServiceInterface service = ((TrackRecordingServiceInterface) mServiceRule.bindService(createStartIntent(context)));
service.startNewTrack();
@@ -313,12 +313,12 @@ public class TrackRecordingServiceTest {
Track.Id trackId = service.getRecordingTrackId();
// when
Marker.Id waypointId = service.insertMarker(null, null, null, null);
Marker.Id markerId = service.insertMarker(null, null, null, null);
// then
assertNotEquals(-1L, waypointId);
Marker wpt = contentProviderUtils.getMarker(waypointId);
assertEquals(context.getString(R.string.marker_waypoint_icon_url), wpt.getIcon());
assertNotEquals(-1L, markerId);
Marker wpt = contentProviderUtils.getMarker(markerId);
assertEquals(context.getString(R.string.marker_icon_url), wpt.getIcon());
assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
assertEquals(trackId, wpt.getTrackId());
assertEquals(0.0, wpt.getLength(), 0.01);
@@ -9,7 +9,7 @@ import org.junit.Test;
*/
public class ElevationSumManagerTest {
private ElevationSumManager elevationSumManager = new ElevationSumManager();
private final ElevationSumManager elevationSumManager = new ElevationSumManager();
private static void addSensorValue(ElevationSumManager elevationSumManager, float[] values) {
for (float f : values) {
@@ -32,7 +32,7 @@ public class ElevationSumManagerTest {
// then
Assert.assertEquals(0f, elevationSumManager.getElevationGain_m(), 0.01);
Assert.assertEquals(-48.0, elevationSumManager.getElevationLoss_m(), 0.01);
Assert.assertEquals(48.0, elevationSumManager.getElevationLoss_m(), 0.01);
}
@Test
@@ -50,9 +50,8 @@ public class AnnouncementUtilsTest {
stats.setMaxSpeed(100);
stats.setTotalElevationGain(6000);
IntervalStatistics intervalStatistics = new IntervalStatistics();
List<TrackPoint> trackPoints = Arrays.asList(TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second);
intervalStatistics.build(trackPoints, 1000);
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, 1000);
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
int speedId = R.plurals.voiceSpeedKilometersPerHour;
@@ -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);
}
}
@@ -43,7 +43,7 @@ public class PressureSensorUtilsTest {
// then
assertEquals(0f, elevationGain_m, 0.01);
assertEquals(-15.0f, elevationLoss_m, 0.01);
assertEquals(15.0f, elevationLoss_m, 0.01);
assertEquals(1016.80f, lastUsedPressureValue_hPa, 0.01); //Expect exponential smoothing
}
}
@@ -226,4 +226,13 @@ public class StringUtilsTest {
assertEquals("min/km", StringUtils.getSpeedParts(context, 0, true, false).second);
assertEquals("min/mi", StringUtils.getSpeedParts(context, 0, false, false).second);
}
@Test
public void testFormatSpeed() {
assertEquals("4:59 min/km", StringUtils.formatSpeed(context, 3.34, true, false));
assertEquals("8:02 min/mi", StringUtils.formatSpeed(context, 3.34, false, false));
assertEquals("12.02 km/h", StringUtils.formatSpeed(context, 3.34, true, true));
assertEquals("7.47 mph", StringUtils.formatSpeed(context, 3.34, false, true));
}
}
@@ -8,6 +8,8 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TrackStatistics;
@@ -57,8 +59,7 @@ public class AggregatedStatisticsTest {
Track track = createTrack(context, totalDistance, totalTime, biking);
// when
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
aggregatedStatistics.aggregate(track);
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(List.of(track));
// then
assertEquals(1, aggregatedStatistics.getCount());
@@ -80,8 +81,7 @@ public class AggregatedStatisticsTest {
Track track = createTrack(context, totalDistance, totalTime, mountainBiking);
// when
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
aggregatedStatistics.aggregate(track);
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(List.of(track));
// then
assertNotNull(aggregatedStatistics.get(mountainBiking));
@@ -97,8 +97,7 @@ public class AggregatedStatisticsTest {
Track track = createTrack(context, totalDistance, totalTime, trailRunning);
// when
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
aggregatedStatistics.aggregate(track);
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(List.of(track));
// then
assertNotNull(aggregatedStatistics.get(trailRunning));
@@ -111,15 +110,10 @@ public class AggregatedStatisticsTest {
long totalDistance = 10000;
long totalTime = 2400000;
String biking = context.getString(R.string.activity_type_biking);
Track[] tracks = new Track[2];
for (int i = 0; i < 2; i++) {
tracks[i] = createTrack(context, totalDistance, totalTime, biking);
}
List<Track> tracks = List.of(createTrack(context, totalDistance, totalTime, biking), createTrack(context, totalDistance, totalTime, biking));
// when
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
aggregatedStatistics.aggregate(tracks[0]); // biking activity 1.
aggregatedStatistics.aggregate(tracks[1]); // biking activity 2.
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(tracks);
// then
assertEquals(1, aggregatedStatistics.getCount());
@@ -141,17 +135,14 @@ public class AggregatedStatisticsTest {
String running = context.getString(R.string.activity_type_running);
String walking = context.getString(R.string.activity_type_walking);
long totalTime = 2400000;
Track[] tracks = new Track[]{
List<Track> tracks = List.of(
createTrack(context, totalDistance, totalTime, biking),
createTrack(context, totalDistance, totalTime, running),
createTrack(context, totalDistance, totalTime, walking)
};
);
// when
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
aggregatedStatistics.aggregate(tracks[0]); // biking activity 1.
aggregatedStatistics.aggregate(tracks[1]); // biking activity 2.
aggregatedStatistics.aggregate(tracks[2]); // biking activity 3.
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(tracks);
// then
assertEquals(3, aggregatedStatistics.getCount());
@@ -191,7 +182,7 @@ public class AggregatedStatisticsTest {
String running = context.getString(R.string.activity_type_running);
String walking = context.getString(R.string.activity_type_walking);
String driving = context.getString(R.string.activity_type_driving);
Track[] tracks = new Track[]{
List<Track> tracks = List.of(
createTrack(context, totalDistance, totalTime, biking),
createTrack(context, totalDistance, totalTime, running),
createTrack(context, totalDistance, totalTime, walking),
@@ -201,16 +192,12 @@ public class AggregatedStatisticsTest {
createTrack(context, totalDistance, totalTime, biking),
createTrack(context, totalDistance, totalTime, biking),
createTrack(context, totalDistance, totalTime, biking),
createTrack(context, totalDistance, totalTime, driving),
};
createTrack(context, totalDistance, totalTime, driving)
);
// when
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics();
for (Track track : tracks) {
aggregatedStatistics.aggregate(track);
}
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(tracks);
// then
// 4 sports.
@@ -255,5 +242,12 @@ public class AggregatedStatisticsTest {
assertEquals(totalDistance, statistics2.getTotalDistance(), 0);
assertEquals(totalTime, statistics2.getMovingTime());
}
// Check order
{
assertEquals(biking, aggregatedStatistics.getItem(0).getCategory());
assertEquals(driving, aggregatedStatistics.getItem(3).getCategory());
}
}
}
@@ -47,10 +47,9 @@ public class IntervalStatisticsTest {
List<TrackPoint> trackPoints = buildTrackPoints(50);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000f;
IntervalStatistics intervalStatistics = new IntervalStatistics();
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval, intervalStatistics);
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
/**
@@ -64,10 +63,9 @@ public class IntervalStatisticsTest {
List<TrackPoint> trackPoints = buildTrackPoints(200);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000f;
IntervalStatistics intervalStatistics = new IntervalStatistics();
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval, intervalStatistics);
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
/**
@@ -80,11 +78,10 @@ public class IntervalStatisticsTest {
// given
List<TrackPoint> trackPoints = buildTrackPoints(200);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 3000;
IntervalStatistics intervalStatistics = new IntervalStatistics();
float distanceInterval = 3000f;
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval, intervalStatistics);
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
/**
@@ -97,11 +94,10 @@ public class IntervalStatisticsTest {
// given
List<TrackPoint> trackPoints = buildTrackPoints(1000);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 3000;
IntervalStatistics intervalStatistics = new IntervalStatistics();
float distanceInterval = 3000f;
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval, intervalStatistics);
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
/**
@@ -114,27 +110,29 @@ public class IntervalStatisticsTest {
// given
List<TrackPoint> trackPoints = buildTrackPoints(10000);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000;
IntervalStatistics intervalStatistics = new IntervalStatistics();
float distanceInterval = 1000f;
// when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval, intervalStatistics);
whenAndThen(trackPoints, trackStatistics, distanceInterval);
}
private void whenAndThen(List<TrackPoint> trackPoints, TrackStatistics trackStatistics, float distanceInterval, IntervalStatistics intervalStatistics) {
intervalStatistics.build(trackPoints, distanceInterval);
private void whenAndThen(List<TrackPoint> trackPoints, TrackStatistics trackStatistics, float distanceInterval) {
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, distanceInterval);
List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList();
double totalDistance = 0d;
long totalTime = 0L;
float totalGain = 0f;
for (IntervalStatistics.Interval i : intervalList) {
totalDistance += i.getDistance_m();
totalTime += ((i.getDistance_m() / i.getSpeed_ms()) * UnitConversions.S_TO_MS);
totalGain += i.getGain_m();
}
// then
assertEquals(trackStatistics.getTotalDistance(), totalDistance, 0.01);
assertEquals(trackStatistics.getTotalTime() * UnitConversions.MS_TO_S, totalTime * UnitConversions.MS_TO_S, 0.1);
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance() / distanceInterval));
assertEquals(totalGain, trackPoints.size() * TestDataUtil.ELEVATION_GAIN, 0.1);
for (int i = 0; i < intervalList.size() - 1; i++) {
assertEquals(intervalList.get(i).getDistance_m(), distanceInterval, 0.001);
totalDistance -= intervalList.get(i).getDistance_m();
+173
View File
@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2"
xmlns:gx="http://www.google.com/kml/ext/2.2"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1">
<Document>
<open>1</open>
<visibility>1</visibility>
<name><![CDATA[2020-11-28 18:06]]></name>
<atom:generator><![CDATA[OpenTracks (Debug)]]></atom:generator>
<Style id="track">
<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>
<IconStyle>
<scale>1.3</scale>
<Icon><href>http://earth.google.com/images/kml-icons/track-directional/track-0.png</href></Icon>
</IconStyle>
</Style>
<Style id="start"><IconStyle>
<scale>1.3</scale>
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/grn-circle.png</href></Icon>
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
</IconStyle></Style>
<Style id="end"><IconStyle>
<scale>1.3</scale>
<Icon><href>http://maps.google.com/mapfiles/kml/paddle/red-circle.png</href></Icon>
<hotSpot x="32" y="1" xunits="pixels" yunits="pixels"/>
</IconStyle></Style>
<Style id="waypoint"><IconStyle>
<scale>1.3</scale>
<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png</href></Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
</IconStyle></Style>
<Schema id="schema">
<gx:SimpleArrayField name="speed" type="float">
<displayName><![CDATA[Speed (m/s)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="power" type="float">
<displayName><![CDATA[Power (W)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="cadence" type="float">
<displayName><![CDATA[Cadence (rpm)]]></displayName>
</gx:SimpleArrayField>
<gx:SimpleArrayField name="heart_rate" type="float">
<displayName><![CDATA[Heart rate (bpm)]]></displayName>
</gx:SimpleArrayField>
</Schema>
<Folder>
<name><![CDATA[Sun, 28 Nov 2020 18:06 Markers]]></name>
<open>1</open>
<Placemark>
<name><![CDATA[Split 0]]></name>
<description><![CDATA[Total distance: 0.00 km (0.0 mi)
Total time: 00:00
Moving time: 00:00
Average speed: 0.00 km/h (0.0 mi/h)
Average moving speed: 0.00 km/h (0.0 mi/h)
Max speed: 0.00 km/h (0.0 mi/h)
Average pace: 0:00 min/km (0:00 min/mi)
Average moving pace: 0:00 min/km (0:00 min/mi)
Fastest pace: 0:00 min/km (0:00 min/mi)
Max elevation: -9223372036854775808 m (-9223372036854775808 ft)
Min elevation: 9223372036854775807 m (9223372036854775807 ft)
Elevation gain: 0 m (0 ft)
Max grade: 0 %
Min grade: 0 %
Recorded: 6/23 11:29
]]></description>
<TimeStamp><when>1970-01-01T00:00:00Z</when></TimeStamp>
<styleUrl>#waypoint</styleUrl>
<Point>
<coordinates>180.0,100.0</coordinates>
</Point>
</Placemark>
</Folder>
<Placemark>
<name><![CDATA[2020-11-28 18:06 (Start)]]></name>
<description><![CDATA[]]></description>
<TimeStamp><when>2020-11-28T17:06:22.401Z</when></TimeStamp>
<styleUrl>#start</styleUrl>
<Point>
<coordinates>1.234156,12.340097,469.286376953125</coordinates>
</Point>
</Placemark>
<Placemark>
<name><![CDATA[2020-11-28 18:06]]></name>
<description><![CDATA[Test Track]]></description>
<icon><![CDATA[UNKNOWN]]></icon>
<opentracks:trackid>2f170875-55d7-445d-b2b8-22a573c81dbc</opentracks:trackid>
<styleUrl>#track</styleUrl>
<ExtendedData>
<Data name="type"><value><![CDATA[unknown]]></value></Data>
</ExtendedData>
<gx:MultiTrack>
<altitudeMode>absolute</altitudeMode>
<gx:interpolate>1</gx:interpolate>
<gx:Track>
<when>2020-11-28T17:06:22.401Z</when>
<gx:coord>1.234156 12.340097 469.286376953125</gx:coord>
<when>2020-11-28T17:06:25.448Z</when>
<gx:coord>1.23415 12.340036 439.1626281738281</gx:coord>
<when>2020-11-28T17:06:47.888Z</when>
<gx:coord>1.23405 12.340057 421.8070983886719</gx:coord>
<ExtendedData>
<SchemaData schemaUrl="#schema">
<gx:SimpleArrayData name="speed">
<gx:value>0.5391469</gx:value>
<gx:value>0.15777911</gx:value>
<gx:value>0.0</gx:value>
</gx:SimpleArrayData>
<gx:SimpleArrayData name="elevation_gain">
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
</gx:SimpleArrayData>
</SchemaData>
</ExtendedData>
</gx:Track>
<gx:Track>
<when>2020-11-28T17:06:55.861Z</when>
<gx:coord>1.23405 12.340057 419.93902587890625</gx:coord>
<when>2020-11-28T17:06:56.905Z</when>
<gx:coord>1.23405 12.340057 419.9036560058594</gx:coord>
<when>2020-11-28T17:07:20.870Z</when>
<gx:coord>1.234046 12.340082 417.99432373046875</gx:coord>
<ExtendedData>
<SchemaData schemaUrl="#schema">
<gx:SimpleArrayData name="speed">
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
</gx:SimpleArrayData>
<gx:SimpleArrayData name="elevation_gain">
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
<gx:value>0.0</gx:value>
</gx:SimpleArrayData>
</SchemaData>
</ExtendedData>
</gx:Track>
</gx:MultiTrack>
</Placemark>
<Placemark>
<name><![CDATA[2020-11-28 18:06 (End)]]></name>
<description><![CDATA[OpenTracks (Debug)
Name: 2020-11-28 18:06
Activity type: unknown
Description: -
Total distance: 0.01 km (0.0 mi)
Total time: 01:04
Moving time: 00:00
Average speed: 0.49 km/h (0.3 mi/h)
Average moving speed: 0.00 km/h (0.0 mi/h)
Max speed: 0.00 km/h (0.0 mi/h)
Average pace: 122:30 min/km (197:08 min/mi)
Average moving pace: 0:00 min/km (0:00 min/mi)
Fastest pace: 0:00 min/km (0:00 min/mi)
Max elevation: 469 m (1540 ft)
Min elevation: 419 m (1376 ft)
Elevation gain: 0 m (0 ft)
Recorded: 11/28 18:06
]]></description>
<TimeStamp><when>2020-11-28T17:07:20.870Z</when></TimeStamp>
<styleUrl>#end</styleUrl>
<Point>
<coordinates>1.234046,12.340082,417.99432373046875</coordinates>
</Point>
</Placemark>
</Document>
</kml>
+200 -50
View File
@@ -50,10 +50,11 @@ limitations under the License.
<application
android:name=".Startup"
android:allowBackup="false"
android:hasFragileUserData="true"
android:hardwareAccelerated="true"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/ThemeCustom">
<activity android:name=".AboutActivity" />
@@ -68,44 +69,188 @@ 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.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.google-earth.kmz" />
<data
android:scheme="content"
android:host="*"
android:pathPattern="*kmz" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.google-earth.kml+xml" />
<data
android:scheme="content"
android:host="*"
android:pathPattern="*kml" />
</intent-filter>
<intent-filter>
<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" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/gpx" />
</intent-filter>
<intent-filter
android:label="@string/app_name"
android:priority="50">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:scheme="data" />
<data android:host="*" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.gpx" />
<data android:pathPattern=".*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
</intent-filter>
<intent-filter
android:label="@string/app_name"
android:priority="50">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:scheme="data" />
<data android:host="*" />
<data android:pathPattern=".*\\.gpx" />
<data android:pathPattern=".*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gpx" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/vnd.google-earth.kmz" />
</intent-filter>
<intent-filter
android:label="@string/app_name"
android:priority="50">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:scheme="data" />
<data android:host="*" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.kmz" />
<data android:pathPattern=".*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
</intent-filter>
<intent-filter
android:label="@string/app_name"
android:priority="50">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:scheme="data" />
<data android:host="*" />
<data android:pathPattern=".*\\.kmz" />
<data android:pathPattern=".*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kmz" />
</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" />
<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>
<activity android:name=".MarkerDetailActivity">
<intent-filter>
<action android:name="de.dennisguse.opentracks.MarkerDetails"/>
<action android:name="de.dennisguse.opentracks.MarkerDetails" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
@@ -114,16 +259,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:launchMode="singleTop">
android:name=".TrackListActivity"
android:launchMode="singleTop"
android:theme="@style/SplashTheme">
<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
@@ -137,25 +290,12 @@ limitations under the License.
<activity android:name=".TrackEditActivity" />
<activity
android:name=".TrackListActivity"
android:theme="@style/SplashTheme">
<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" />
@@ -195,5 +335,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,7 +17,9 @@
package de.dennisguse.opentracks;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
@@ -35,14 +37,25 @@ 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);
setupActionBarBack(toolbar);
}
/**
* Gets the layout resource id.
*/
protected abstract int getLayoutResId();
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
protected abstract View getRootView();
protected void setupActionBarBack(@Nullable Toolbar toolbar) {
setSupportActionBar(toolbar);
if (toolbar != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
}
}
@@ -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,24 +30,19 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.databinding.MarkerEditBinding;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.viewmodels.MarkerEditViewModel;
/**
* An activity to add/edit a marker.
@@ -59,131 +54,77 @@ public class MarkerEditActivity extends AbstractActivity {
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
private static final String BUNDLE_PHOTO_URI = "photo_uri";
private static final int CAMERA_REQUEST_CODE = 5;
private static final int GALLERY_IMG_REQUEST_CODE = 7;
private static final String TAG = MarkerEditActivity.class.getSimpleName();
private Track.Id trackId;
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
private Marker waypoint;
private Marker marker;
private MenuItem insertPhotoMenuItem;
private MenuItem insertGalleryImgMenuItem;
private Uri photoUri;
private boolean hasCamera;
private Uri cameraPhotoUri;
private MarkerEditViewModel viewModel;
// 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);
}
waypointPhoto.setImageBitmap(null);
photoUri = null;
hideAndShowOptions();
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.marker_types, android.R.layout.simple_dropdown_item_1line);
viewBinding.markerEditMarkerType.setAdapter(adapter);
viewBinding.markerEditPhotoDelete.setOnClickListener(v -> {
viewModel.onPhotoDelete(viewBinding.markerEditName.getText().toString(),
viewBinding.markerEditMarkerType.getText().toString(),
viewBinding.markerEditDescription.getText().toString());
});
Button cancel = findViewById(R.id.marker_edit_cancel);
cancel.setOnClickListener(v -> finish());
final boolean isNewMarker = waypointId == null;
setTitle(isNewMarker ? R.string.menu_insert_marker : R.string.menu_edit);
Button done = findViewById(R.id.marker_edit_done);
done.setText(isNewMarker ? R.string.generic_add : R.string.generic_save);
done.setOnClickListener(v -> {
if (isNewMarker) {
addMarker();
} else {
saveMarker();
}
viewBinding.markerEditCancel.setOnClickListener(v -> {
viewModel.onCancel();
finish();
});
if (isNewMarker) {
int nextWaypointNumber = trackId == null ? -1 : new ContentProviderUtils(this).getNextMarkerNumber(trackId);
if (nextWaypointNumber == -1) {
nextWaypointNumber = 0;
}
waypointName.setText(getString(R.string.marker_name_format, nextWaypointNumber));
waypointName.selectAll();
waypointMarkerType.setText("");
waypointDescription.setText("");
} else {
waypoint = new ContentProviderUtils(this).getMarker(waypointId);
if (waypoint == null) {
Log.d(TAG, "waypoint is null");
finish();
return;
}
waypointName.setText(waypoint.getName());
waypointMarkerType.setText(waypoint.getCategory());
waypointDescription.setText(waypoint.getDescription());
if (waypoint.hasPhoto()) {
photoUri = waypoint.getPhotoURI();
}
}
boolean isNewMarker = markerId == null;
setTitle(isNewMarker ? R.string.menu_insert_marker : R.string.menu_edit);
viewBinding.markerEditDone.setText(isNewMarker ? R.string.generic_add : R.string.generic_save);
viewBinding.markerEditDone.setOnClickListener(v -> {
viewModel.onDone(viewBinding.markerEditName.getText().toString(),
viewBinding.markerEditMarkerType.getText().toString(),
viewBinding.markerEditDescription.getText().toString());
finish();
});
if (savedInstanceState != null) {
photoUri = savedInstanceState.getParcelable(BUNDLE_PHOTO_URI);
if (waypoint != null) {
waypoint.setPhotoUrl(photoUri != null ? photoUri.toString() : null);
viewModel = new ViewModelProvider(this).get(MarkerEditViewModel.class);
viewModel.getMarkerData(trackId, markerId).observe(this, data -> {
marker = data;
viewBinding.markerEditName.setText(marker.getName());
viewBinding.markerEditMarkerType.setText(marker.getCategory());
viewBinding.markerEditDescription.setText(marker.getDescription());
if (marker.hasPhoto()) {
setMarkerImageView(marker.getPhotoURI());
} else {
viewBinding.markerEditPhoto.setImageDrawable(null);
}
}
if (photoUri != null) {
setWaypointImageView(photoUri);
}
hideAndShowOptions();
hideAndShowOptions();
});
}
@Override
protected void onStart() {
super.onStart();
trackRecordingServiceConnection.startConnection(this);
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(BUNDLE_PHOTO_URI, photoUri);
}
@Override
protected void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind(this);
}
@Override
protected int getLayoutResId() {
return R.layout.marker_edit;
protected View getRootView() {
viewBinding = MarkerEditBinding.inflate(getLayoutInflater());
return viewBinding.getRoot();
}
@Override
@@ -200,16 +141,17 @@ public class MarkerEditActivity extends AbstractActivity {
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.marker_edit_insert_photo:
createWaypointWithPicture();
return true;
case R.id.marker_edit_insert_gallery_img:
createWaypointWithGalleryImage();
return true;
default:
return super.onOptionsItemSelected(item);
if (item.getItemId() == R.id.marker_edit_insert_photo) {
createMarkerWithPicture();
return true;
}
if (item.getItemId() == R.id.marker_edit_insert_gallery_img) {
createMarkerWithGalleryImage();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
@@ -219,26 +161,20 @@ 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);
viewModel.onNewCameraPhoto(cameraPhotoUri,
viewBinding.markerEditName.getText().toString(),
viewBinding.markerEditMarkerType.getText().toString(),
viewBinding.markerEditDescription.getText().toString());
}
} else if (requestCode == GALLERY_IMG_REQUEST_CODE) {
if (resultCode == RESULT_CANCELED) {
Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show();
return;
} else if (resultCode == RESULT_OK) {
Uri srcUri = data.getData();
try (ParcelFileDescriptor parcelFd = getContentResolver().openFileDescriptor(srcUri, "r")) {
FileDescriptor srcFd = parcelFd.getFileDescriptor();
File dstFile = new File(FileUtils.getImageUrl(this, getTrackId()));
FileUtils.copy(srcFd, dstFile);
photoUri = FileUtils.getUriForFile(this, dstFile);
setWaypointImageView(photoUri);
} catch(Exception e) {
Log.e(TAG, e.getMessage());
Toast.makeText(this, R.string.marker_add_canceled, Toast.LENGTH_LONG).show();
return;
}
} else if (resultCode == RESULT_OK) {
viewModel.onNewGalleryPhoto(data.getData(),
viewBinding.markerEditName.getText().toString(),
viewBinding.markerEditMarkerType.getText().toString(),
viewBinding.markerEditDescription.getText().toString());
}
}
super.onActivityResult(requestCode, resultCode, data);
@@ -246,35 +182,30 @@ public class MarkerEditActivity extends AbstractActivity {
/**
* Checks and hide/shows all buttons/options about marker photo options.
*
* <p>
* If a photo is set then one's options are shown, otherwise another ones are shown.
*/
private void hideAndShowOptions() {
boolean isPhotoSet = (waypoint != null && waypoint.hasPhoto()) || photoUri != null;
boolean isPhotoSet = (marker != null && marker.hasPhoto());
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 +213,14 @@ public class MarkerEditActivity extends AbstractActivity {
}
}
private void createWaypointWithPicture() {
private void createMarkerWithPicture() {
Pair<Intent, Uri> intentAndPhotoUri = IntentUtils.createTakePictureIntent(this, getTrackId());
photoUri = intentAndPhotoUri.second;
cameraPhotoUri = 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(),
photoUri != null ? photoUri.toString() : null);
}
private void saveMarker() {
waypoint.setName(waypointName.getText().toString());
waypoint.setCategory(waypointMarkerType.getText().toString());
waypoint.setDescription(waypointDescription.getText().toString());
waypoint.setPhotoUrl(photoUri != null ? photoUri.toString() : null);
new ContentProviderUtils(this).updateMarker(this, waypoint);
}
}
@@ -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,22 +82,22 @@ 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
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isSingleSelection = ids.length == 1;
menu.findItem(R.id.list_context_menu_share).setVisible(false);
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection);
menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection);
menu.findItem(R.id.list_context_menu_delete).setVisible(true);
/*
* Set select all to the same visibility as delete since delete is the
* only action that can be applied to multiple markers.
*/
// Set select all to the same visibility as delete since delete is the only action that can be applied to multiple markers.
menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll);
}
@@ -120,9 +121,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 +135,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 +142,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 +172,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,44 +209,54 @@ 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]));
}
return true;
case R.id.list_context_menu_edit:
if (waypointIds.length == 1) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, waypointIds[0]);
startActivity(intent);
}
return true;
case R.id.list_context_menu_delete:
if (waypointIds.length > 1 && waypointIds.length == listView.getCount()) {
waypointIds = null;
}
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), waypointIds);
return true;
case R.id.list_context_menu_select_all:
for (int i = 0; i < listView.getCount(); i++) {
listView.setItemChecked(i, true);
}
return false;
default:
return false;
if (itemId == R.id.list_context_menu_show_on_map) {
if (markerIds.length == 1) {
IntentUtils.showCoordinateOnMap(this, contentProviderUtils.getMarker(markerIds[0]));
}
return true;
}
if (itemId == R.id.list_context_menu_share) {
Intent intent = IntentUtils.newShareFileIntent(this, markerIds);
if (intent != null) {
intent = Intent.createChooser(intent, null);
startActivity(intent);
}
return true;
}
if (itemId == R.id.list_context_menu_edit) {
if (markerIds.length == 1) {
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerIds[0]);
startActivity(intent);
}
return true;
}
if (itemId == R.id.list_context_menu_delete) {
DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), markerIds);
return true;
}
if (itemId == R.id.list_context_menu_select_all) {
for (int i = 0; i < viewBinding.markerList.getCount(); i++) {
viewBinding.markerList.setItemChecked(i, true);
}
return false;
}
return false;
}
@Override
@@ -278,7 +268,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().equals(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,13 @@ 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.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import android.view.accessibility.AccessibilityEvent;
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 +36,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;
view.setPressed(false);
transition.resetTransition();
showStatusSetDefaultText();
return false;
}
//Trigger button pressed animation
view.setPressed(true);
//To pause a recording
if (viewBinding.trackControllerRecord.equals(view)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& isRecording && !isPaused) {
transition.startTransition(buttonDelayDuration);
ActivityUtils.vibrate(activity, 150);
viewBinding.trackControllerStatus.setText(R.string.hold_to_pause);
buttonDelay = () -> {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
view.performClick();
callback.recordStart(); //TODO Should be recordPause();
transition.resetTransition();
ActivityUtils.vibrate(activity, 1000);
};
handlerUpdateTotalTime.postDelayed(buttonDelay, buttonDelayDuration);
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 +169,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 +210,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 +224,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,19 @@ public class TrackDeleteActivity extends AbstractActivity {
}
@Override
protected int getLayoutResId() {
return R.layout.track_delete;
protected void onDestroy() {
super.onDestroy();
viewBinding = null;
}
@Override
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) {
@@ -80,20 +72,17 @@ public class TrackEditActivity extends AbstractActivity implements ChooseActivit
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,21 +33,20 @@ import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ListView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.cursoradapter.widget.ResourceCursorAdapter;
import androidx.loader.app.LoaderManager;
import androidx.loader.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 +65,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 +96,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 +121,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 +133,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,45 +167,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);
@@ -237,9 +191,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.
@@ -256,14 +209,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);
@@ -272,7 +226,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);
@@ -282,15 +236,20 @@ 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();
}
@Override
protected void setupActionBarBack(Toolbar toolbar) {
setSupportActionBar(toolbar);
toolbar.setNavigationIcon(R.drawable.ic_logo_color_24dp);
}
@Override
protected void onStart() {
super.onStart();
@@ -300,6 +259,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();
@@ -310,14 +277,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();
@@ -338,8 +297,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
@@ -367,43 +327,45 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent intent;
switch (item.getItemId()) {
case R.id.track_list_start_gps:
LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
if (locationManager != null && !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(intent);
if (item.getItemId() == R.id.track_list_start_gps) {
LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
if (locationManager != null && !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
} else {
// Invoke trackRecordingService
if (!isGpsStarted()) {
trackRecordingServiceConnection.startAndBind(this);
bindChangedCallback.run();
} else {
// Invoke trackRecordingService
if (!isGpsStarted()) {
trackRecordingServiceConnection.startAndBind(this);
bindChangedCallback.run();
} else {
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
trackRecordingService.stopGps();
}
trackRecordingServiceConnection.unbindAndStop(this);
TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound();
if (trackRecordingService != null) {
trackRecordingService.stopGps();
}
// Update menu after starting or stopping gps
this.invalidateOptionsMenu();
trackRecordingServiceConnection.unbindAndStop(this);
}
return true;
case R.id.track_list_aggregated_stats:
intent = IntentUtils.newIntent(this, AggregatedStatisticsActivity.class);
startActivity(intent);
return true;
case R.id.track_list_markers:
intent = IntentUtils.newIntent(this, MarkerListActivity.class);
startActivity(intent);
return true;
case R.id.track_list_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
return true;
// Update menu after starting or stopping gps
this.invalidateOptionsMenu();
}
return true;
}
if (item.getItemId() == R.id.track_list_aggregated_stats) {
startActivity(IntentUtils.newIntent(this, AggregatedStatisticsActivity.class));
return true;
}
if (item.getItemId() == R.id.track_list_markers) {
startActivity(IntentUtils.newIntent(this, MarkerListActivity.class));
return true;
}
if (item.getItemId() == R.id.track_list_settings) {
startActivity(IntentUtils.newIntent(this, SettingsActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
@@ -415,6 +377,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
@@ -428,7 +420,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
}
@Override
protected void onDeleted() {
protected void onTrackDeleted() {
// Do nothing
}
@@ -484,31 +476,116 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
trackIds[i] = new Track.Id(longTrackIds[i]);
}
Intent intent;
switch (itemId) {
case R.id.list_context_menu_show_on_map:
IntentDashboardUtils.startDashboard(this, false, trackIds);
return true;
case R.id.list_context_menu_share:
intent = IntentUtils.newShareFileIntent(this, trackIds);
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.list_context_menu_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
startActivity(intent);
return true;
case R.id.list_context_menu_delete:
deleteTracks(trackIds);
return true;
case R.id.list_context_menu_select_all:
int size = listView.getCount();
for (int i = 0; i < size; i++) {
listView.setItemChecked(i, true);
}
return false;
if (itemId == R.id.list_context_menu_show_on_map) {
IntentDashboardUtils.startDashboard(this, false, trackIds);
return true;
}
if (itemId == R.id.list_context_menu_share) {
Intent intent = IntentUtils.newShareFileIntent(this, trackIds);
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
}
if (itemId == R.id.list_context_menu_edit) {
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
startActivity(intent);
return true;
}
if (itemId == R.id.list_context_menu_delete) {
deleteTracks(trackIds);
return true;
}
if (itemId == R.id.list_context_menu_select_all) {
for (int i = 0; i < viewBinding.trackList.getCount(); i++) {
viewBinding.trackList.setItemChecked(i, true);
}
return false;
}
return false;
}
private class TrackLoaderCallBack implements LoaderManager.LoaderCallbacks<Cursor> {
private String searchQuery = null;
public String getSearchQuery() {
return searchQuery;
}
public void setSearch(String searchQuery) {
this.searchQuery = searchQuery;
restart();
if (searchQuery != null) {
setTitle(searchQuery);
} else {
setTitle(R.string.app_name);
}
}
public void restart() {
LoaderManager.getInstance(TrackListActivity.this).restartLoader(0, null, loaderCallbacks);
}
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME,
TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME,
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON, TracksColumns.MARKER_COUNT};
final String sortOrder = TracksColumns.STARTTIME + " DESC";
if (searchQuery == null) {
return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, null, null, sortOrder);
} else {
final String SEARCH_QUERY = TracksColumns.NAME + " LIKE ? OR " +
TracksColumns.DESCRIPTION + " LIKE ? OR " +
TracksColumns.CATEGORY + " LIKE ?";
final String[] selectionArgs = new String[]{"%" + searchQuery + "%", "%" + searchQuery + "%", "%" + searchQuery + "%"};
return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, SEARCH_QUERY, selectionArgs, sortOrder);
}
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
resourceCursorAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
resourceCursorAdapter.swapCursor(null);
}
}
@Override
public void recordStart() {
if (!PreferencesUtils.isRecording(recordingTrackId)) {
// Not recording -> Recording
updateMenuItems(false, true);
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackRecordingActivity.class);
startActivity(newIntent);
} else if (recordingTrackPaused) {
// Paused -> Resume
updateMenuItems(false, true);
trackRecordingServiceConnection.resumeTrack();
trackController.update(true, false);
} else {
// Recording -> Paused
updateMenuItems(false, true);
trackRecordingServiceConnection.pauseTrack();
trackController.update(true, true);
}
}
@Override
public void recordStop() {
updateMenuItems(false, false);
trackRecordingServiceConnection.stopRecording(TrackListActivity.this, true);
}
}
@@ -21,21 +21,21 @@ 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 androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
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.IntervalsFragment;
import de.dennisguse.opentracks.fragments.StatisticsRecordedFragment;
@@ -44,8 +44,6 @@ 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 +51,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 +77,12 @@ 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);
CustomFragmentPagerAdapter pagerAdapter = new CustomFragmentPagerAdapter(this);
viewBinding.trackDetailActivityViewPager.setAdapter(pagerAdapter);
new TabLayoutMediator(viewBinding.trackDetailActivityTablayout, viewBinding.trackDetailActivityViewPager,
(tab, position) -> tab.setText(pagerAdapter.getPageTitle(position))).attach();
if (savedInstanceState != null) {
pager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
viewBinding.trackDetailActivityViewPager.setCurrentItem(savedInstanceState.getInt(CURRENT_TAB_TAG_KEY));
}
}
@@ -100,8 +99,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 +113,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,49 +139,58 @@ 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);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent intent;
switch (item.getItemId()) {
case R.id.track_detail_share:
intent = IntentUtils.newShareFileIntent(this, track.getId());
intent = Intent.createChooser(intent, null);
startActivity(intent);
return true;
case R.id.track_detail_menu_show_on_map:
IntentDashboardUtils.startDashboard(this, false, track.getId());
return true;
case R.id.track_detail_markers:
intent = IntentUtils.newIntent(this, MarkerListActivity.class)
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, track.getId());
startActivity(intent);
return true;
case R.id.track_detail_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, track.getId());
startActivity(intent);
return true;
case R.id.track_detail_delete:
deleteTracks(track.getId());
return true;
case R.id.track_detail_resume_track:
Intent newIntent = IntentUtils.newIntent(TrackRecordedActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, track.getId());
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
finish();
return true;
case R.id.track_detail_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
if (item.getItemId() == R.id.track_detail_share) {
Intent intent = Intent.createChooser(IntentUtils.newShareFileIntent(this, trackId), null);
startActivity(intent);
return true;
}
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.startDashboard(this, false, trackId);
return true;
}
if (item.getItemId() == R.id.track_detail_markers) {
Intent intent = IntentUtils.newIntent(this, MarkerListActivity.class)
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
}
if (item.getItemId() == R.id.track_detail_edit) {
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
}
if (item.getItemId() == R.id.track_detail_delete) {
deleteTracks(trackId);
return true;
}
if (item.getItemId() == R.id.track_detail_resume_track) {
Intent newIntent = IntentUtils.newIntent(TrackRecordedActivity.this, TrackRecordingActivity.class)
.putExtra(TrackRecordingActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
finish();
return true;
}
if (item.getItemId() == R.id.track_detail_settings) {
startActivity(IntentUtils.newIntent(this, SettingsActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
@@ -191,7 +200,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
}
@Override
protected void onDeleted() {
protected void onTrackDeleted() {
runOnUiThread(this::finish);
}
@@ -204,56 +213,39 @@ 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);
}
private class CustomFragmentPagerAdapter extends FragmentStateAdapter {
@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 {
public CustomFragmentPagerAdapter(@NonNull FragmentManager fm, int behavior) {
super(fm, behavior);
}
@Override
public int getCount() {
return 4;
public CustomFragmentPagerAdapter(@NonNull FragmentActivity fa) {
super(fa);
}
@NonNull
@Override
public Fragment getItem(int position) {
public Fragment createFragment(int position) {
switch (position) {
case 0:
return StatisticsRecordedFragment.newInstance(track.getId());
return StatisticsRecordedFragment.newInstance(trackId);
case 1:
return IntervalsFragment.newInstance(track.getId());
return IntervalsFragment.newInstance();
case 2:
return ChartFragment.newInstance(false);
case 3:
@@ -264,6 +256,10 @@ public class TrackRecordedActivity extends AbstractListActivity implements Choos
}
@Override
public int getItemCount() {
return 4;
}
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
@@ -9,20 +9,19 @@ 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 androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.databinding.TrackRecordingBinding;
import de.dennisguse.opentracks.fragments.ChartFragment;
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
import de.dennisguse.opentracks.fragments.IntervalsFragment;
@@ -44,7 +43,7 @@ import de.dennisguse.opentracks.util.TrackUtils;
*/
//NOTE: This activity does NOT react to preference changes of R.string.recording_track_id_key.
//This mode of communication should be removed anyhow.
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface {
public class TrackRecordingActivity extends AbstractActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, TrackActivityDataHubInterface, TrackController.Callback {
public static final String EXTRA_TRACK_ID = "track_id";
@@ -57,18 +56,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 +82,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 +127,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 +146,19 @@ 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);
CustomFragmentPagerAdapter pagerAdapter = new CustomFragmentPagerAdapter(this);
viewBinding.trackDetailActivityViewPager.setAdapter(pagerAdapter);
new TabLayoutMediator(viewBinding.trackDetailActivityTablayout, viewBinding.trackDetailActivityViewPager,
(tab, position) -> tab.setText(pagerAdapter.getPageTitle(position))).attach();
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 +236,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 +255,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
@@ -314,34 +278,41 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent intent;
switch (item.getItemId()) {
case R.id.track_detail_insert_marker:
intent = IntentUtils
.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_menu_show_on_map:
IntentDashboardUtils.startDashboard(this, true, trackId);
return true;
case R.id.track_detail_markers:
intent = IntentUtils.newIntent(this, MarkerListActivity.class)
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
if (item.getItemId() == R.id.track_detail_insert_marker) {
Intent intent = IntentUtils
.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
}
if (item.getItemId() == R.id.track_detail_menu_show_on_map) {
IntentDashboardUtils.startDashboard(this, true, trackId);
return true;
}
if (item.getItemId() == R.id.track_detail_markers) {
Intent intent = IntentUtils.newIntent(this, MarkerListActivity.class)
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
}
if (item.getItemId() == R.id.track_detail_edit) {
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
}
if (item.getItemId() == R.id.track_detail_settings) {
Intent intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}
/**
@@ -372,25 +343,46 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
TrackUtils.updateTrack(this, track, null, category, null, contentProviderUtils);
}
private class CustomFragmentPagerAdapter extends FragmentPagerAdapter {
public CustomFragmentPagerAdapter(@NonNull FragmentManager fm, int behavior) {
super(fm, behavior);
@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 int getCount() {
return 4;
@Override
public void recordStop() {
trackRecordingServiceConnection.stopRecording(TrackRecordingActivity.this, true);
Intent newIntent = IntentUtils.newIntent(TrackRecordingActivity.this, TrackRecordedActivity.class)
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
startActivity(newIntent);
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
updateMenuItems(true);
finish();
}
private class CustomFragmentPagerAdapter extends FragmentStateAdapter {
public CustomFragmentPagerAdapter(@NonNull FragmentActivity fa) {
super(fa);
}
@NonNull
@Override
public Fragment getItem(int position) {
public Fragment createFragment(int position) {
switch (position) {
case 0:
return StatisticsRecordingFragment.newInstance();
case 1:
return IntervalsFragment.IntervalsRecordingFragment.newInstance(trackId);
return IntervalsFragment.IntervalsRecordingFragment.newInstance();
case 2:
return ChartFragment.newInstance(false);
case 3:
@@ -401,6 +393,10 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
@Override
public int getItemCount() {
return 4;
}
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
@@ -416,15 +412,4 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
}
}
}
public interface OnTrackIdListener {
void onTrackId(Track.Id trackId);
}
public void setTrackIdListener(OnTrackIdListener listener) {
this.intervalsListener = listener;
if (trackId != null) {
listener.onTrackId(trackId);
}
}
}
}
@@ -46,49 +46,45 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
AggregatedStatistics.AggregatedStatistic aggregatedStats = getItem(position);
ViewHolder viewHolder;
AggregatedStatistics.AggregatedStatistic aggregatedStatistic = getItem(position);
if (isSpeedSport(position)) {
viewHolder = new ViewSpeedHolder();
ViewHolder viewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.aggregated_stats_list_item, parent, false);
viewHolder = new ViewHolder(convertView);
convertView.setTag(viewHolder);
} else {
viewHolder = new ViewPaceHolder();
viewHolder = (ViewHolder) convertView.getTag();
}
convertView = LayoutInflater.from(context).inflate(R.layout.aggregated_stats_list_item, parent, false);
viewHolder.setValues(convertView, getIcon(position), getName(position), aggregatedStats);
if (TrackIconUtils.isSpeedIcon(context, aggregatedStatistic.getCategory())) {
viewHolder.setSpeed(aggregatedStatistic);
} else {
viewHolder.setPace(aggregatedStatistic);
}
return convertView;
}
private boolean isSpeedSport(int position) {
String category = aggregatedStatistics.getSportName(position);
return TrackIconUtils.isSpeedIcon(context, category);
}
private int getIcon(int position) {
String iconValue = TrackIconUtils.getIconValue(context, aggregatedStatistics.getSportName(position));
return TrackIconUtils.getIconDrawable(iconValue);
}
private String getName(int position) {
return aggregatedStatistics.getSportName(position);
}
private class ViewHolder {
protected ImageView sportIcon;
protected TextView typeLabel;
protected TextView numTracks;
protected TextView distance;
protected TextView distanceUnit;
protected TextView time;
protected boolean metricsUnits;
protected boolean reportSpeed;
private final ImageView sportIcon;
private final TextView typeLabel;
private final TextView numTracks;
private final TextView distance;
private final TextView distanceUnit;
private final TextView time;
public void setValues(View view, int iconDrawable, String name, AggregatedStatistics.AggregatedStatistic aggregatedStats) {
reportSpeed = PreferencesUtils.isReportSpeed(context, name);
metricsUnits = PreferencesUtils.isMetricUnits(context);
private final TextView avgSpeed;
private final TextView avgSpeedUnit;
private final TextView avgSpeedLabel;
private final TextView maxSpeed;
private final TextView maxSpeedUnit;
private final TextView maxSpeedLabel;
private boolean metricsUnits;
private boolean reportSpeed;
public ViewHolder(View view) {
sportIcon = view.findViewById(R.id.aggregated_stats_sport_icon);
typeLabel = view.findViewById(R.id.aggregated_stats_type_label);
numTracks = view.findViewById(R.id.aggregated_stats_num_tracks);
@@ -96,86 +92,68 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
distanceUnit = view.findViewById(R.id.aggregated_stats_distance_unit);
time = view.findViewById(R.id.aggregated_stats_time);
sportIcon.setImageResource(iconDrawable);
typeLabel.setText(name);
numTracks.setText(StringUtils.valueInParentheses(String.valueOf(aggregatedStats.getCountTracks())));
Pair<String, String> parts = StringUtils.getDistanceParts(context, aggregatedStats.getTrackStatistics().getTotalDistance(), metricsUnits);
distance.setText(parts.first);
distanceUnit.setText(parts.second);
time.setText(StringUtils.formatElapsedTime(aggregatedStats.getTrackStatistics().getMovingTime()));
}
}
private class ViewSpeedHolder extends ViewHolder {
private TextView avgSpeed;
private TextView avgSpeedUnit;
private TextView avgSpeedLabel;
private TextView maxSpeed;
private TextView maxSpeedUnit;
private TextView maxSpeedLabel;
@Override
public void setValues(View view, int iconDrawable, String name, AggregatedStatistics.AggregatedStatistic aggregatedStats) {
super.setValues(view, iconDrawable, name, aggregatedStats);
avgSpeed = view.findViewById(R.id.aggregated_stats_avg_rate);
avgSpeedUnit = view.findViewById(R.id.aggregated_stats_avg_rate_unit);
avgSpeedLabel = view.findViewById(R.id.aggregated_stats_avg_rate_label);
maxSpeed = view.findViewById(R.id.aggregated_stats_max_rate);
maxSpeedUnit = view.findViewById(R.id.aggregated_stats_max_rate_unit);
maxSpeedLabel = view.findViewById(R.id.aggregated_stats_max_rate_label);
}
public void setSpeed(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
setCommonValues(aggregatedStatistic);
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, reportSpeed);
avgSpeed.setText(parts.first);
avgSpeedUnit.setText(parts.second);
avgSpeedLabel.setText(context.getString(R.string.stats_average_moving_speed));
}
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getMaxSpeed(), metricsUnits, reportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getMaxSpeed(), metricsUnits, reportSpeed);
maxSpeed.setText(parts.first);
maxSpeedUnit.setText(parts.second);
maxSpeedLabel.setText(context.getString(R.string.stats_max_speed));
}
}
}
private class ViewPaceHolder extends ViewHolder {
private TextView avgPace;
private TextView avgPaceUnit;
private TextView avgPaceLabel;
private TextView maxPace;
private TextView maxPaceUnit;
private TextView maxPaceLabel;
@Override
public void setValues(View view, int iconDrawable, String name, AggregatedStatistics.AggregatedStatistic aggregatedStats) {
super.setValues(view, iconDrawable, name, aggregatedStats);
avgPace = view.findViewById(R.id.aggregated_stats_avg_rate);
avgPaceUnit = view.findViewById(R.id.aggregated_stats_avg_rate_unit);
avgPaceLabel = view.findViewById(R.id.aggregated_stats_avg_rate_label);
maxPace = view.findViewById(R.id.aggregated_stats_max_rate);
maxPaceUnit = view.findViewById(R.id.aggregated_stats_max_rate_unit);
maxPaceLabel = view.findViewById(R.id.aggregated_stats_max_rate_label);
public void setPace(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
setCommonValues(aggregatedStatistic);
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, reportSpeed);
avgPace.setText(parts.first);
avgPaceUnit.setText(parts.second);
avgPaceLabel.setText(R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, reportSpeed);
avgSpeed.setText(parts.first);
avgSpeedUnit.setText(parts.second);
avgSpeedLabel.setText(context.getString(R.string.stats_average_moving_pace));
}
{
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStats.getTrackStatistics().getMaxSpeed(), metricsUnits, reportSpeed);
maxPace.setText(parts.first);
maxPaceUnit.setText(parts.second);
maxPaceLabel.setText(R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getMaxSpeed(), metricsUnits, reportSpeed);
maxSpeed.setText(parts.first);
maxSpeedUnit.setText(parts.second);
maxSpeedLabel.setText(R.string.stats_fastest_pace);
}
}
private void setCommonValues(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
String category = aggregatedStatistic.getCategory();
reportSpeed = PreferencesUtils.isReportSpeed(context, category);
metricsUnits = PreferencesUtils.isMetricUnits(context);
sportIcon.setImageResource(getIcon(aggregatedStatistic));
typeLabel.setText(category);
numTracks.setText(StringUtils.valueInParentheses(String.valueOf(aggregatedStatistic.getCountTracks())));
Pair<String, String> parts = StringUtils.getDistanceParts(context, aggregatedStatistic.getTrackStatistics().getTotalDistance(), metricsUnits);
distance.setText(parts.first);
distanceUnit.setText(parts.second);
time.setText(StringUtils.formatElapsedTime(aggregatedStatistic.getTrackStatistics().getMovingTime()));
}
private int getIcon(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
String iconValue = TrackIconUtils.getIconValue(context, aggregatedStatistic.getCategory());
return TrackIconUtils.getIconDrawable(iconValue);
}
}
}
@@ -1,7 +1,6 @@
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;
@@ -20,13 +19,14 @@ import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.Interval> {
private StackMode stackMode;
private boolean metricUnits;
private float sumDistance_m;
private final StackMode stackMode;
private final boolean metricUnits;
private final String category;
public IntervalStatisticsAdapter(Context context, List<IntervalStatistics.Interval> intervalList, StackMode stackMode) {
public IntervalStatisticsAdapter(Context context, List<IntervalStatistics.Interval> intervalList, String category, StackMode stackMode) {
super(context, R.layout.interval_stats_list_item, intervalList);
metricUnits = PreferencesUtils.isMetricUnits(context);
this.category = category;
this.stackMode = stackMode;
}
@@ -43,14 +43,16 @@ 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);
viewHolder.loss = intervalView.findViewById(R.id.interval_item_loss);
intervalView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) intervalView.getTag();
}
float sumDistance_m;
if (actualPosition + 1 == getCount() && actualPosition > 0) {
sumDistance_m = actualPosition * getItem(actualPosition - 1).getDistance_m() + interval.getDistance_m();
} else {
@@ -58,11 +60,14 @@ public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.I
}
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)) {
viewHolder.rate.setText(StringUtils.formatSpeed(getContext(), interval.getSpeed_ms(), metricUnits, true));
} else {
viewHolder.rate.setText(StringUtils.formatSpeed(getContext(), interval.getSpeed_ms(), metricUnits, false));
}
Pair<String, String> paceParts = StringUtils.getSpeedParts(getContext(), interval.getSpeed_ms(), metricUnits, false);
viewHolder.pace.setText(paceParts.first + " " + paceParts.second);
viewHolder.gain.setText(StringUtils.formatDistance(getContext(), interval.getGain_m(), metricUnits));
viewHolder.loss.setText(StringUtils.formatDistance(getContext(), interval.getLoss_m(), metricUnits));
return intervalView;
}
@@ -72,12 +77,13 @@ public class IntervalStatisticsAdapter extends ArrayAdapter<IntervalStatistics.I
*/
public enum StackMode {
STACK_FROM_BOTTOM,
STACK_FROM_TOP;
STACK_FROM_TOP
}
private static class ViewHolder {
private TextView distance;
private TextView speed;
private TextView pace;
private TextView rate;
private TextView gain;
private TextView loss;
}
}
@@ -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;
@@ -68,7 +68,7 @@ abstract class ChartValueSeries {
* @param fillColor the fill color
* @param strokeColor the stroke color
*/
ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, int metricTitleId, int imperialTitleId, int fillColor, int strokeColor) {
ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues, int metricTitleId, int imperialTitleId, int fillColor, int strokeColor, int fontSizeSmall, int fontSizeMedium) {
this.absoluteMin = absoluteMin;
this.absoluteMax = absoluteMax;
this.intervalValues = intervalValues;
@@ -85,23 +85,19 @@ abstract class ChartValueSeries {
strokePaint.setColor(context.getResources().getColor(strokeColor));
strokePaint.setAntiAlias(true);
float scale = context.getResources().getDisplayMetrics().density;
// Make copies of the stroke paint with the default thickness
titlePaint = new Paint(strokePaint);
titlePaint.setTextSize(ChartView.MEDIUM_TEXT_SIZE * scale);
titlePaint.setTextSize(fontSizeMedium);
titlePaint.setTextAlign(Align.CENTER);
titlePaint.setStyle(Style.FILL_AND_STROKE);
markerPaint = new Paint(strokePaint);
markerPaint.setTextSize(ChartView.SMALL_TEXT_SIZE * scale);
markerPaint.setTextSize(fontSizeSmall);
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;
@@ -31,9 +30,11 @@ import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewParent;
import android.widget.Scroller;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.view.GestureDetectorCompat;
import java.text.NumberFormat;
@@ -47,6 +48,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;
/**
@@ -58,9 +60,6 @@ import de.dennisguse.opentracks.util.UnitConversions;
*/
public class ChartView extends View {
static final float MEDIUM_TEXT_SIZE = 18f;
static final float SMALL_TEXT_SIZE = 12f;
static final int Y_AXIS_INTERVALS = 5;
private static final int TARGET_X_AXIS_INTERVALS = 4;
@@ -75,7 +74,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 +86,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;
@@ -113,7 +113,7 @@ public class ChartView extends View {
private int effectiveWidth = 0;
private int effectiveHeight = 0;
private boolean chartByDistance;
private final boolean chartByDistance;
private boolean metricUnits = true;
private boolean reportSpeed = true;
private boolean showPointer = false;
@@ -150,19 +150,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,6 +191,8 @@ public class ChartView extends View {
public ChartView(Context context, final boolean chartByDistance) {
super(context);
this.chartByDistance = chartByDistance;
int fontSizeSmall = ThemeUtils.getFontSizeSmallInPx(context);
int fontSizeMedium = ThemeUtils.getFontSizeMediumInPx(context);
seriesList.add(new ChartValueSeries(context,
Integer.MIN_VALUE,
@@ -199,7 +201,9 @@ public class ChartView extends View {
R.string.description_elevation_metric,
R.string.description_elevation_imperial,
R.color.chart_elevation_fill,
R.color.chart_elevation_border) {
R.color.chart_elevation_border,
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getElevation();
@@ -218,7 +222,9 @@ public class ChartView extends View {
R.string.description_speed_metric,
R.string.description_speed_imperial,
R.color.chart_speed_fill,
R.color.chart_speed_border) {
R.color.chart_speed_border,
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getSpeed();
@@ -238,7 +244,9 @@ public class ChartView extends View {
R.string.description_pace_metric,
R.string.description_pace_imperial,
R.color.chart_pace_fill,
R.color.chart_pace_border) {
R.color.chart_pace_border,
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getPace();
@@ -258,7 +266,9 @@ public class ChartView extends View {
R.string.description_sensor_heart_rate,
R.string.description_sensor_heart_rate,
R.color.chart_heart_rate_fill,
R.color.chart_heart_rate_border) {
R.color.chart_heart_rate_border,
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getHeartRate();
@@ -277,7 +287,9 @@ public class ChartView extends View {
R.string.description_sensor_cadence,
R.string.description_sensor_cadence,
R.color.chart_cadence_fill,
R.color.chart_cadence_border) {
R.color.chart_cadence_border,
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getCadence();
@@ -295,7 +307,9 @@ public class ChartView extends View {
R.string.description_sensor_power,
R.string.description_sensor_power,
R.color.chart_power_fill,
R.color.chart_power_border) {
R.color.chart_power_border,
fontSizeSmall,
fontSizeMedium) {
@Override
protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getPower();
@@ -307,35 +321,34 @@ public class ChartView extends View {
}
});
float scale = context.getResources().getDisplayMetrics().density;
backgroundColor = ThemeUtils.getBackgroundColor(context);
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);
axisPaint.setTextSize(fontSizeSmall);
xAxisMarkerPaint = new Paint(axisPaint);
xAxisMarkerPaint.setTextAlign(Align.CENTER);
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 = ContextCompat.getDrawable(context, 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 +447,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();
}
}
@@ -488,6 +501,18 @@ public class ChartView extends View {
invalidate();
}
/**
* Handle parent's view disallow touch event.
*
* @param disallow Does disallow parent touch event?
*/
private void requestDisallowInterceptTouchEventInParent(boolean disallow) {
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(disallow);
}
}
/**
* Scrolls the view horizontally by a given amount.
*
@@ -495,13 +520,15 @@ public class ChartView extends View {
*/
private void scrollBy(int deltaX) {
int scrollX = getScrollX() + deltaX;
if (scrollX < 0) {
if (scrollX <= 0) {
scrollX = 0;
}
int maxWidth = effectiveWidth * (zoomLevel - 1);
if (scrollX > maxWidth) {
if (scrollX >= maxWidth) {
scrollX = maxWidth;
}
scrollTo(scrollX, 0);
}
@@ -526,6 +553,10 @@ public class ChartView extends View {
public boolean onTouchEvent(MotionEvent event) {
boolean isZoom = detectorZoom.onTouchEvent(event);
boolean isScrollTab = detectorScrollFlingTab.onTouchEvent(event);
// ChartView handles zoom gestures (more than one pointer) and all gestures when zoomed itself
requestDisallowInterceptTouchEventInParent(event.getPointerCount() != 1 || zoomLevel != MIN_ZOOM_LEVEL);
return isZoom || isScrollTab;
}
@@ -540,13 +571,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 +618,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();
}
}
@@ -885,6 +911,7 @@ public class ChartView extends View {
/**
* Finds the index of the first data point containing data for a series.
*
* @return -1 if no data point contains data for the series.
*/
private int getFirstPopulatedChartDataIndex(ChartValueSeries chartValueSeries) {
@@ -904,7 +931,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 +1002,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();
}
}
@@ -140,6 +140,11 @@ public class DescriptionGenerator {
writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
}
// Elevation loss
if (stats.hasTotalElevationLoss()) {
writeElevation(stats.getTotalElevationLoss(), builder, R.string.description_elevation_loss, lineBreak);
}
// Recorded time
builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime_ms())));
builder.append(lineBreak);
@@ -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().equals(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,33 @@ 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(@Nullable Track.Id trackId) {
this.trackId = trackId;
location = null;
}
@VisibleForTesting
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 +82,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 +125,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 +175,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
@@ -33,6 +33,7 @@ public class TrackPoint {
private Float cyclingCadence_rpm = null;
private Float power = null;
private Float elevationGain = null;
private Float elevationLoss = null;
public TrackPoint() {
this.location = new Location("");
@@ -48,6 +49,9 @@ public class TrackPoint {
this.heartRate_bpm = trackPoint.getHeartRate_bpm();
this.cyclingCadence_rpm = trackPoint.getCyclingCadence_rpm();
this.power = trackPoint.getPower();
this.elevationGain = trackPoint.getElevationGain();
this.elevationLoss = trackPoint.getElevationLoss();
}
public TrackPoint(double latitude, double longitude, Double altitude, long time) {
@@ -105,6 +109,18 @@ public class TrackPoint {
this.elevationGain = elevationGain;
}
public boolean hasElevationLoss() {
return elevationLoss != null;
}
public float getElevationLoss() {
return elevationLoss;
}
public void setElevationLoss(Float elevationLoss) {
this.elevationLoss = elevationLoss;
}
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";
@@ -52,10 +54,11 @@ public interface TrackPointsColumns extends BaseColumns {
String SENSOR_CADENCE = "sensor_cadence";
String SENSOR_POWER = "sensor_power";
String ELEVATION_GAIN = "elevation_gain";
String ELEVATION_LOSS = "elevation_loss";
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ TRACKID + " INTEGER, "
+ TRACKID + " INTEGER NOT NULL, "
+ LONGITUDE + " INTEGER, "
+ LATITUDE + " INTEGER, "
+ TIME + " INTEGER, "
@@ -66,7 +69,10 @@ public interface TrackPointsColumns extends BaseColumns {
+ SENSOR_HEARTRATE + " FLOAT, "
+ SENSOR_CADENCE + " FLOAT, "
+ SENSOR_POWER + " FLOAT, "
+ ELEVATION_GAIN + " FLOAT)";
+ ELEVATION_GAIN + " FLOAT, "
+ ELEVATION_LOSS + " 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
@@ -53,27 +54,29 @@ public interface TracksColumns extends BaseColumns {
String MINELEVATION = "minelevation"; // minimum elevation
String MAXELEVATION = "maxelevation"; // maximum elevation
String ELEVATIONGAIN = "elevationgain"; // elevation gain
String ELEVATIONLOSS = "elevationloss"; // elevation loss
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, "
+ ELEVATIONLOSS + " FLOAT)";
String CREATE_TABLE_INDEX = "CREATE UNIQUE INDEX " + TABLE_NAME + "_" + UUID + "_index ON " + TABLE_NAME + "(" + UUID + ")";
@@ -20,6 +20,7 @@ class CachedTrackPointsIndexes {
final int sensorCadenceIndex;
final int sensorPowerIndex;
final int elevationGainIndex;
final int elevationLossIndex;
CachedTrackPointsIndexes(Cursor cursor) {
idIndex = cursor.getColumnIndex(TrackPointsColumns._ID);
@@ -34,5 +35,6 @@ class CachedTrackPointsIndexes {
sensorCadenceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_CADENCE);
sensorPowerIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_POWER);
elevationGainIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ELEVATION_GAIN);
elevationLossIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ELEVATION_LOSS);
}
}
@@ -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);
@@ -121,6 +114,7 @@ public class ContentProviderUtils {
int minElevationIndex = cursor.getColumnIndexOrThrow(TracksColumns.MINELEVATION);
int maxElevationIndex = cursor.getColumnIndexOrThrow(TracksColumns.MAXELEVATION);
int elevationGainIndex = cursor.getColumnIndexOrThrow(TracksColumns.ELEVATIONGAIN);
int elevationLossIndex = cursor.getColumnIndexOrThrow(TracksColumns.ELEVATIONLOSS);
int iconIndex = cursor.getColumnIndexOrThrow(TracksColumns.ICON);
Track track = new Track();
@@ -167,6 +161,9 @@ public class ContentProviderUtils {
if (!cursor.isNull(elevationGainIndex)) {
trackStatistics.setTotalElevationGain(cursor.getFloat(elevationGainIndex));
}
if (!cursor.isNull(elevationLossIndex)) {
trackStatistics.setTotalElevationLoss(cursor.getFloat(elevationLossIndex));
}
if (!cursor.isNull(iconIndex)) {
track.setIcon(cursor.getString(iconIndex));
}
@@ -184,17 +181,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));
@@ -209,7 +207,6 @@ public class ContentProviderUtils {
contentResolver.delete(MarkerColumns.CONTENT_URI, MarkerColumns.TRACKID + "=?", selectionArgs);
}
@VisibleForTesting
public List<Track> getTracks() {
ArrayList<Track> tracks = new ArrayList<>();
try (Cursor cursor = getTrackCursor(null, null, TracksColumns._ID)) {
@@ -223,27 +220,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
public Track getLastTrack() {
try (Cursor cursor = getTrackCursor(null, null, TracksColumns.STARTTIME + " DESC")) {
// Using the same order as shown in the track list
if (cursor != null && cursor.moveToNext()) {
return createTrack(cursor);
}
}
return null;
}
/**
* @param trackId the track id.
*/
@@ -328,6 +304,7 @@ public class ContentProviderUtils {
values.put(TracksColumns.MINELEVATION, trackStatistics.getMinElevation());
values.put(TracksColumns.MAXELEVATION, trackStatistics.getMaxElevation());
values.put(TracksColumns.ELEVATIONGAIN, trackStatistics.getTotalElevationGain());
values.put(TracksColumns.ELEVATIONLOSS, trackStatistics.getTotalElevationLoss());
values.put(TracksColumns.ICON, track.getIcon());
return values;
@@ -368,7 +345,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 +363,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 +404,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 +412,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 +450,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;
@@ -532,7 +479,7 @@ public class ContentProviderUtils {
private void deleteMarkerPhoto(Context context, Marker marker) {
if (marker != null && marker.hasPhoto()) {
Uri uri = marker.getPhotoURI();
File file = FileUtils.getPhotoFileIfExists(context, marker.getTrackId(), uri);
File file = FileUtils.buildInternalPhotoFile(context, marker.getTrackId(), uri);
if (file.exists()) {
File parent = file.getParentFile();
file.delete();
@@ -566,9 +513,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());
@@ -651,6 +596,9 @@ public class ContentProviderUtils {
if (!cursor.isNull(indexes.elevationGainIndex)) {
trackPoint.setElevationGain(cursor.getFloat(indexes.elevationGainIndex));
}
if (!cursor.isNull(indexes.elevationLossIndex)) {
trackPoint.setElevationLoss(cursor.getFloat(indexes.elevationLossIndex));
}
return trackPoint;
}
@@ -662,7 +610,7 @@ public class ContentProviderUtils {
* @param trackId the trackPoints id
* @return the number of trackPoints inserted
*/
//TODO Only used for testing and file import; might be better to replace it.
//TODO Only used for testing; might be better to replace it.
public int bulkInsertTrackPoint(TrackPoint[] trackPoints, Track.Id trackId) {
ContentValues[] values = new ContentValues[trackPoints.length];
for (int i = 0; i < values.length; i++) {
@@ -671,6 +619,15 @@ public class ContentProviderUtils {
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
}
//TODO Only used for file import; might be better to replace it.
public int bulkInsertTrackPoint(List<TrackPoint> trackPoints, Track.Id trackId) {
ContentValues[] values = new ContentValues[trackPoints.size()];
for (int i = 0; i < trackPoints.size(); i++) {
values[i] = createContentValues(trackPoints.get(i), trackId);
}
return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI_BY_ID, values);
}
/**
* Gets the first location id for a track.
* Returns null if it doesn't exist.
@@ -844,6 +801,9 @@ public class ContentProviderUtils {
if (trackPoint.hasElevationGain()) {
values.put(TrackPointsColumns.ELEVATION_GAIN, trackPoint.getElevationGain());
}
if (trackPoint.hasElevationLoss()) {
values.put(TrackPointsColumns.ELEVATION_LOSS, trackPoint.getElevationLoss());
}
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 = 27;
private static final int DATABASE_VERSION = 29;
@VisibleForTesting
public static final String DATABASE_NAME = "database.db";
@@ -73,7 +73,12 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 27:
upgradeFrom26to27(db);
break;
case 28:
upgradeFrom27to28(db);
break;
case 29:
upgradeFrom28to29(db);
break;
default:
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
@@ -98,6 +103,12 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 26:
downgradeFrom27to26(db);
break;
case 27:
downgradeFrom28to27(db);
break;
case 28:
downgradeFrom29to28(db);
break;
default:
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
@@ -142,6 +153,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.endTransaction();
}
/**
* Add indeces for foreign key trackId
*/
private void upgradeFrom24to25(SQLiteDatabase db) {
db.beginTransaction();
@@ -162,6 +176,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();
@@ -198,6 +215,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.endTransaction();
}
/**
* Add elevation gain
*/
private void upgradeFrom26to27(SQLiteDatabase db) {
db.beginTransaction();
@@ -220,4 +240,94 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
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();
}
/**
* Add elevation loss.
*/
private void upgradeFrom28to29(SQLiteDatabase db) {
db.beginTransaction();
db.execSQL("ALTER TABLE tracks ADD COLUMN elevationloss FLOAT");
db.execSQL("ALTER TABLE trackpoints ADD COLUMN elevation_loss FLOAT");
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom29to28(SQLiteDatabase db) {
db.beginTransaction();
// Tracks
db.execSQL("DROP INDEX tracks_uuid_index");
db.execSQL("ALTER TABLE tracks RENAME TO tracks_old");
db.execSQL("CREATE TABLE tracks (_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)");
db.execSQL("INSERT INTO tracks SELECT _id, name, description, category, starttime, stoptime, numpoints, totaldistance, totaltime, movingtime, avgspeed, avgmovingspeed, maxspeed, minelevation, maxelevation, elevationgain, icon, uuid FROM tracks_old");
db.execSQL("DROP TABLE tracks_old");
db.execSQL("CREATE UNIQUE INDEX tracks_uuid_index ON tracks(uuid)");
// TrackPoints
db.execSQL("DROP INDEX trackpoints_trackid_index");
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)");
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;
}
}
}

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