From 99265afe5868b61e338a6792fd95a185b41407c6 Mon Sep 17 00:00:00 2001 From: y20k Date: Thu, 2 Jan 2020 18:00:37 +0100 Subject: [PATCH] Kotlin Rewrite - everything is new --- .gitignore | 74 +- LICENSE.md | 2 +- README.md | 4 +- app/build.gradle | 83 +- app/proguard-rules.pro | 16 +- app/src/main/AndroidManifest.xml | 54 +- app/src/main/ic_launcher-web.png | Bin 26942 -> 25989 bytes app/src/main/java/org/y20k/trackbook/Keys.kt | 134 +++ .../java/org/y20k/trackbook/MainActivity.java | 925 ------------------ .../java/org/y20k/trackbook/MainActivity.kt | 85 ++ .../trackbook/MainActivityMapFragment.java | 741 -------------- .../trackbook/MainActivityTrackFragment.java | 729 -------------- .../java/org/y20k/trackbook/MapFragment.kt | 289 ++++++ .../org/y20k/trackbook/SettingsFragment.kt | 109 +++ .../java/org/y20k/trackbook/TrackFragment.kt | 143 +++ .../java/org/y20k/trackbook/Trackbook.java | 59 -- .../java/org/y20k/trackbook/Trackbook2.kt | 51 + .../org/y20k/trackbook/TrackerService.java | 618 ------------ .../java/org/y20k/trackbook/TrackerService.kt | 356 +++++++ .../org/y20k/trackbook/TracklistFragment.kt | 161 +++ .../java/org/y20k/trackbook/core/Track.java | 337 ------- .../java/org/y20k/trackbook/core/Track.kt | 77 ++ .../org/y20k/trackbook/core/TrackBuilder.java | 87 -- .../org/y20k/trackbook/core/TrackBundle.java | 85 -- .../java/org/y20k/trackbook/core/Tracklist.kt | 53 + .../y20k/trackbook/core/TracklistElement.kt | 46 + .../org/y20k/trackbook/core/WayPoint.java | 124 --- .../java/org/y20k/trackbook/core/WayPoint.kt | 54 + .../org/y20k/trackbook/dialogs/ErrorDialog.kt | 92 ++ .../trackbook/dialogs/RenameTrackDialog.kt | 82 ++ .../org/y20k/trackbook/dialogs/YesNoDialog.kt | 97 ++ .../extensions/SharedPreferencesExt.kt | 29 + .../y20k/trackbook/helpers/DateTimeHelper.kt | 84 ++ .../y20k/trackbook/helpers/DialogHelper.java | 87 -- .../trackbook/helpers/DropdownAdapter.java | 187 ---- .../y20k/trackbook/helpers/ExportHelper.java | 224 ----- .../org/y20k/trackbook/helpers/FileHelper.kt | 431 ++++++++ .../y20k/trackbook/helpers/ImportHelper.kt | 165 ++++ .../trackbook/helpers/LengthUnitHelper.java | 96 -- .../trackbook/helpers/LengthUnitHelper.kt | 95 ++ .../trackbook/helpers/LocationHelper.java | 302 ------ .../y20k/trackbook/helpers/LocationHelper.kt | 218 +++++ .../org/y20k/trackbook/helpers/LogHelper.java | 62 -- .../org/y20k/trackbook/helpers/LogHelper.kt | 115 +++ .../org/y20k/trackbook/helpers/MapHelper.java | 275 ------ .../org/y20k/trackbook/helpers/MapHelper.kt | 148 +++ .../trackbook/helpers/NightModeHelper.java | 177 ---- .../y20k/trackbook/helpers/NightModeHelper.kt | 150 +++ .../trackbook/helpers/NotificationHelper.java | 171 ---- .../trackbook/helpers/NotificationHelper.kt | 135 +++ .../trackbook/helpers/PreferencesHelper.kt | 167 ++++ .../y20k/trackbook/helpers/StorageHelper.java | 405 -------- .../org/y20k/trackbook/helpers/TrackHelper.kt | 194 ++++ .../y20k/trackbook/helpers/TrackbookKeys.java | 122 --- .../org/y20k/trackbook/helpers/UiHelper.kt | 130 +++ .../layout/DodgeAbleLayoutBehavior.java | 60 -- .../layout/NonSwipeableViewPager.java | 102 -- .../trackbook/tracklist/TracklistAdapter.kt | 208 ++++ .../trackbook/ui/MapFragmentLayoutHolder.kt | 233 +++++ .../trackbook/ui/TrackFragmentLayoutHolder.kt | 254 +++++ .../drawable-v24/ic_launcher_foreground.xml | 34 + .../res/drawable/ic_account_circle_black.xml | 8 + .../drawable/ic_compass_needle_black_24dp.xml | 2 +- ..._24dp.xml => ic_current_location_24dp.xml} | 2 +- ...te_forever_24dp.xml => ic_delete_24dp.xml} | 2 +- app/src/main/res/drawable/ic_edit_24dp.xml | 9 + .../main/res/drawable/ic_email_black_24dp.xml | 9 + app/src/main/res/drawable/ic_info_24dp.xml | 2 +- .../res/drawable/ic_launcher_background.xml | 74 ++ ...p.xml => ic_marker_location_blue_24dp.xml} | 0 ... => ic_marker_location_blue_grey_24dp.xml} | 0 ...dp.xml => ic_marker_location_red_24dp.xml} | 0 ...l => ic_marker_location_red_grey_24dp.xml} | 0 ...=> ic_marker_track_location_blue_24dp.xml} | 0 ...=> ic_marker_track_location_grey_24dp.xml} | 0 ... => ic_marker_track_location_red_24dp.xml} | 0 ...arker_track_location_transparent_24dp.xml} | 0 .../ic_notification_action_resume_36dp.xml | 9 + .../ic_notification_action_show_36dp.xml | 9 + .../ic_notification_action_stop_24dp.xml | 9 + ...ation_icon_large_tracking_active_48dp.xml} | 0 ...tion_icon_large_tracking_stopped_48dp.xml} | 0 ...ml => ic_notification_icon_small_24dp.xml} | 0 .../res/drawable/ic_notifications_black.xml | 9 + .../res/drawable/ic_person_black_24dp.xml | 9 + .../ic_question_answer_black_24dp.xml | 9 + .../res/drawable/ic_remove_circle_24dp.xml | 9 + .../main/res/drawable/ic_settings_black.xml | 9 + .../res/drawable/ic_settings_black_24dp.xml | 9 + app/src/main/res/drawable/ic_share_24dp.xml | 2 +- app/src/main/res/drawable/ic_star_24dp.xml | 9 + .../main/res/drawable/ic_star_border_24dp.xml | 9 + .../drawable/selector_bottom_navigation.xml | 3 +- .../shape_statistics_background_collapsed.xml | 21 + .../shape_statistics_background_expanded.xml | 21 + app/src/main/res/layout/activity_main.xml | 236 +---- .../layout/custom_dropdown_item_collapsed.xml | 13 - .../layout/custom_dropdown_item_expanded.xml | 13 - .../layout/dialog_generic_with_details.xml | 57 ++ .../main/res/layout/dialog_rename_track.xml | 31 + app/src/main/res/layout/fragment_main_map.xml | 17 - app/src/main/res/layout/fragment_map.xml | 216 ++++ ...ment_main_track.xml => fragment_track.xml} | 42 +- .../main/res/layout/fragment_tracklist.xml | 33 + .../res/layout/fragment_tracklist_land.xml | 65 ++ app/src/main/res/layout/main_onboarding.xml | 12 +- app/src/main/res/layout/track_element.xml | 66 ++ app/src/main/res/layout/track_management.xml | 62 -- app/src/main/res/layout/track_onboarding.xml | 43 +- app/src/main/res/layout/track_statistics.xml | 234 +++-- ...nu_main.xml => menu_bottom_navigation.xml} | 11 +- app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 2515 -> 1772 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 3561 -> 2787 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 4935 -> 4022 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1739 -> 1236 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 2256 -> 1608 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2999 -> 2402 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 3553 -> 2687 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 4977 -> 4168 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 6734 -> 5778 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 5645 -> 4476 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 8554 -> 7763 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 10657 -> 9412 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 7734 -> 6608 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 12208 -> 11295 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 15172 -> 13894 bytes .../main/res/navigation/nav_graph_main.xml | 56 ++ .../res/navigation/nav_graph_tracklist.xml | 22 + app/src/main/res/values-da/strings.xml | 15 +- app/src/main/res/values-de/strings.xml | 15 +- app/src/main/res/values-fr/strings.xml | 15 +- app/src/main/res/values-id/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 15 +- app/src/main/res/values-ja/strings.xml | 15 +- app/src/main/res/values-nb-rNO/strings.xml | 15 +- app/src/main/res/values-night-v23/styles.xml | 24 - app/src/main/res/values-night/colors.xml | 27 +- app/src/main/res/values-night/styles.xml | 13 +- app/src/main/res/values-nl/strings.xml | 15 +- app/src/main/res/values-sv/strings.xml | 4 +- app/src/main/res/values-sw600dp/bools.xml | 4 + app/src/main/res/values-v23/styles.xml | 24 - app/src/main/res/values-w820dp/dimens.xml | 7 - app/src/main/res/values-zh-rCN/strings.xml | 13 +- app/src/main/res/values/bools.xml | 4 + app/src/main/res/values/colors.xml | 52 +- app/src/main/res/values/strings.xml | 109 ++- app/src/main/res/values/styles.xml | 24 +- app/src/main/res/xml/backupscheme.xml | 5 - app/src/main/res/xml/provider_paths.xml | 8 +- assets/trackbook-app-icon-current.svg | 22 +- build.gradle | 31 +- gradle.properties | 15 +- gradlew | 100 +- gradlew.bat | 14 +- 155 files changed, 6115 insertions(+), 6802 deletions(-) create mode 100644 app/src/main/java/org/y20k/trackbook/Keys.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/MainActivity.java create mode 100644 app/src/main/java/org/y20k/trackbook/MainActivity.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/MainActivityMapFragment.java delete mode 100755 app/src/main/java/org/y20k/trackbook/MainActivityTrackFragment.java create mode 100644 app/src/main/java/org/y20k/trackbook/MapFragment.kt create mode 100644 app/src/main/java/org/y20k/trackbook/SettingsFragment.kt create mode 100644 app/src/main/java/org/y20k/trackbook/TrackFragment.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/Trackbook.java create mode 100644 app/src/main/java/org/y20k/trackbook/Trackbook2.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/TrackerService.java create mode 100644 app/src/main/java/org/y20k/trackbook/TrackerService.kt create mode 100644 app/src/main/java/org/y20k/trackbook/TracklistFragment.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/core/Track.java create mode 100644 app/src/main/java/org/y20k/trackbook/core/Track.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/core/TrackBuilder.java delete mode 100755 app/src/main/java/org/y20k/trackbook/core/TrackBundle.java create mode 100644 app/src/main/java/org/y20k/trackbook/core/Tracklist.kt create mode 100644 app/src/main/java/org/y20k/trackbook/core/TracklistElement.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/core/WayPoint.java create mode 100644 app/src/main/java/org/y20k/trackbook/core/WayPoint.kt create mode 100644 app/src/main/java/org/y20k/trackbook/dialogs/ErrorDialog.kt create mode 100644 app/src/main/java/org/y20k/trackbook/dialogs/RenameTrackDialog.kt create mode 100644 app/src/main/java/org/y20k/trackbook/dialogs/YesNoDialog.kt create mode 100644 app/src/main/java/org/y20k/trackbook/extensions/SharedPreferencesExt.kt create mode 100644 app/src/main/java/org/y20k/trackbook/helpers/DateTimeHelper.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/helpers/DialogHelper.java delete mode 100755 app/src/main/java/org/y20k/trackbook/helpers/DropdownAdapter.java delete mode 100755 app/src/main/java/org/y20k/trackbook/helpers/ExportHelper.java create mode 100644 app/src/main/java/org/y20k/trackbook/helpers/FileHelper.kt create mode 100644 app/src/main/java/org/y20k/trackbook/helpers/ImportHelper.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.java create mode 100644 app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.java create mode 100644 app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/helpers/LogHelper.java create mode 100644 app/src/main/java/org/y20k/trackbook/helpers/LogHelper.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/helpers/MapHelper.java create mode 100644 app/src/main/java/org/y20k/trackbook/helpers/MapHelper.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.java create mode 100644 app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.java create mode 100644 app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.kt create mode 100644 app/src/main/java/org/y20k/trackbook/helpers/PreferencesHelper.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/helpers/StorageHelper.java create mode 100644 app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/helpers/TrackbookKeys.java create mode 100644 app/src/main/java/org/y20k/trackbook/helpers/UiHelper.kt delete mode 100755 app/src/main/java/org/y20k/trackbook/layout/DodgeAbleLayoutBehavior.java delete mode 100755 app/src/main/java/org/y20k/trackbook/layout/NonSwipeableViewPager.java create mode 100644 app/src/main/java/org/y20k/trackbook/tracklist/TracklistAdapter.kt create mode 100644 app/src/main/java/org/y20k/trackbook/ui/MapFragmentLayoutHolder.kt create mode 100644 app/src/main/java/org/y20k/trackbook/ui/TrackFragmentLayoutHolder.kt create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_account_circle_black.xml rename app/src/main/res/drawable/{ic_my_location_24dp.xml => ic_current_location_24dp.xml} (90%) rename app/src/main/res/drawable/{ic_delete_forever_24dp.xml => ic_delete_24dp.xml} (88%) create mode 100644 app/src/main/res/drawable/ic_edit_24dp.xml create mode 100644 app/src/main/res/drawable/ic_email_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml rename app/src/main/res/drawable/{ic_my_location_dot_blue_24dp.xml => ic_marker_location_blue_24dp.xml} (100%) rename app/src/main/res/drawable/{ic_my_location_dot_blue_grey_24dp.xml => ic_marker_location_blue_grey_24dp.xml} (100%) rename app/src/main/res/drawable/{ic_my_location_dot_red_24dp.xml => ic_marker_location_red_24dp.xml} (100%) rename app/src/main/res/drawable/{ic_my_location_dot_red_grey_24dp.xml => ic_marker_location_red_grey_24dp.xml} (100%) rename app/src/main/res/drawable/{ic_my_location_crumb_blue_24dp.xml => ic_marker_track_location_blue_24dp.xml} (100%) rename app/src/main/res/drawable/{ic_my_location_crumb_grey_24dp.xml => ic_marker_track_location_grey_24dp.xml} (100%) rename app/src/main/res/drawable/{ic_my_location_crumb_red_24dp.xml => ic_marker_track_location_red_24dp.xml} (100%) rename app/src/main/res/drawable/{ic_my_location_crumb_transparent_24dp.xml => ic_marker_track_location_transparent_24dp.xml} (100%) create mode 100644 app/src/main/res/drawable/ic_notification_action_resume_36dp.xml create mode 100644 app/src/main/res/drawable/ic_notification_action_show_36dp.xml create mode 100644 app/src/main/res/drawable/ic_notification_action_stop_24dp.xml rename app/src/main/res/drawable/{ic_notification_large_tracking_48dp.xml => ic_notification_icon_large_tracking_active_48dp.xml} (100%) rename app/src/main/res/drawable/{ic_notification_large_not_tracking_48dp.xml => ic_notification_icon_large_tracking_stopped_48dp.xml} (100%) rename app/src/main/res/drawable/{ic_notification_small_24dp.xml => ic_notification_icon_small_24dp.xml} (100%) create mode 100644 app/src/main/res/drawable/ic_notifications_black.xml create mode 100644 app/src/main/res/drawable/ic_person_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_question_answer_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_remove_circle_24dp.xml create mode 100644 app/src/main/res/drawable/ic_settings_black.xml create mode 100644 app/src/main/res/drawable/ic_settings_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_star_24dp.xml create mode 100644 app/src/main/res/drawable/ic_star_border_24dp.xml create mode 100644 app/src/main/res/drawable/shape_statistics_background_collapsed.xml create mode 100644 app/src/main/res/drawable/shape_statistics_background_expanded.xml mode change 100755 => 100644 app/src/main/res/layout/activity_main.xml delete mode 100755 app/src/main/res/layout/custom_dropdown_item_collapsed.xml delete mode 100755 app/src/main/res/layout/custom_dropdown_item_expanded.xml create mode 100644 app/src/main/res/layout/dialog_generic_with_details.xml create mode 100644 app/src/main/res/layout/dialog_rename_track.xml delete mode 100755 app/src/main/res/layout/fragment_main_map.xml create mode 100644 app/src/main/res/layout/fragment_map.xml rename app/src/main/res/layout/{fragment_main_track.xml => fragment_track.xml} (54%) create mode 100644 app/src/main/res/layout/fragment_tracklist.xml create mode 100644 app/src/main/res/layout/fragment_tracklist_land.xml create mode 100644 app/src/main/res/layout/track_element.xml delete mode 100755 app/src/main/res/layout/track_management.xml rename app/src/main/res/menu/{menu_main.xml => menu_bottom_navigation.xml} (50%) mode change 100755 => 100644 create mode 100644 app/src/main/res/navigation/nav_graph_main.xml create mode 100644 app/src/main/res/navigation/nav_graph_tracklist.xml delete mode 100755 app/src/main/res/values-night-v23/styles.xml create mode 100644 app/src/main/res/values-sw600dp/bools.xml delete mode 100755 app/src/main/res/values-v23/styles.xml delete mode 100755 app/src/main/res/values-w820dp/dimens.xml create mode 100644 app/src/main/res/values/bools.xml mode change 100755 => 100644 app/src/main/res/values/colors.xml mode change 100755 => 100644 app/src/main/res/values/strings.xml mode change 100755 => 100644 app/src/main/res/values/styles.xml delete mode 100755 app/src/main/res/xml/backupscheme.xml mode change 100755 => 100644 app/src/main/res/xml/provider_paths.xml diff --git a/.gitignore b/.gitignore index a090e65..f8d5865 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,68 @@ +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ *.iml -.gradle -/local.properties -/.idea -.DS_Store -/build -/captures -/projectFilesBackup \ No newline at end of file +.idea/ +#.idea/workspace.xml +#.idea/tasks.xml +#.idea/gradle.xml +#.idea/assetWizardSettings.xml +#.idea/dictionaries +#.idea/libraries +#.idea/caches + +# Keystore files +*.jks + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Misc +.DS_Store \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index c5f69d4..0f1b989 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,7 @@ The MIT License (MIT) ===================== -Copyright (c) 2016-19 - Y20K.org +Copyright (c) 2016-20 - Y20K.org -------------------------------- Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.md b/README.md index c39548f..bee390a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ README Trackbook - Movement Recorder for Android ----------------------------------------- -**Version 1.2.x ("San Tropez")** +**Version 2.0.x ("Echoes")** + +**Please note: Trackbook is currently being completely re-written in Kotlin. No line of code is left unchanged. The process is not finished yet.** Trackbook is a bare bones app for recording your movements. Trackbook is great for hiking, vacation or workout. Once started it traces your movements on a map. The map data is provided by [OpenStreetMap (OSM)](https://www.openstreetmap.org/). diff --git a/app/build.gradle b/app/build.gradle index d85e175..d01fd94 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,48 +1,89 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'androidx.navigation.safeargs.kotlin' android { - - compileSdkVersion project.ext.compileSdkVersion + compileSdkVersion 29 // buildToolsVersion is optional because the plugin uses a recommended version by default defaultConfig { - applicationId project.ext.applicationId - minSdkVersion project.ext.minSdkVersion - targetSdkVersion project.ext.targetSdkVersion - versionCode project.ext.versionCode - versionName project.ext.versionName + applicationId 'org.y20k.trackbook' + minSdkVersion 25 + targetSdkVersion 27 + versionCode 27 + versionName '2.0.0' + resConfigs "en" + } - vectorDrawables.useSupportLibrary = true - resConfigs "en", "da", "de", "fr", "id", "it", "ja", "nb-rNO", "nl", "sv", "zh-rCN" + kotlinOptions { + jvmTarget = '1.8' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + lintOptions{ + disable 'MissingTranslation' } buildTypes { - release { + // Enables code shrinking, obfuscation, and optimization for only + // your project's release build type. minifyEnabled true + + // Enables resource shrinking, which is performed by the + // Android Gradle plugin. shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + + // Includes the default ProGuard rules files that are packaged with + // the Android Gradle plugin. To learn more, go to the section about + // R8 configuration files. + proguardFiles getDefaultProguardFile( + 'proguard-android-optimize.txt'), + 'proguard-rules.pro' } - } + debug { + // Comment out the below lines if you do not need to test resource shrinking + //minifyEnabled true + //shrinkResources true + //proguardFiles getDefaultProguardFile( + // 'proguard-android-optimize.txt'), + // 'proguard-rules.pro' + } - lintOptions { - warning 'MissingTranslation' } } dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.2" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.2" - implementation "androidx.appcompat:appcompat:$appcompatVersion" - implementation "androidx.constraintlayout:constraintlayout:$constraintlayoutVersion" - implementation "androidx.cardview:cardview:$cardviewVersion" + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation "androidx.core:core-ktx:1.1.0" + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' + implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0' + implementation "androidx.preference:preference-ktx:1.1.0" - implementation "com.google.android.material:material:$materialVersion" + implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0' + implementation 'androidx.navigation:navigation-ui-ktx:2.1.0' - implementation "org.osmdroid:osmdroid-android:$osmdroidVersion" - implementation "com.google.code.gson:gson:$gsonVersion" + implementation "com.google.android.material:material:1.1.0-beta01" + implementation "com.google.code.gson:gson:2.8.6" + + implementation "org.osmdroid:osmdroid-android:6.1.5" +} + +androidExtensions { + experimental = true } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 55837c4..f1b4245 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,14 +1,10 @@ # Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in /Users/solaris/Library/Android/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html -# Add any project specific keep options here: - # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: @@ -16,6 +12,10 @@ # public *; #} +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable -# stop re-ordering of gson elements --dontshrink \ No newline at end of file +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 11e6b47..1199564 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,10 +1,11 @@ + xmlns:tools="http://schemas.android.com/tools" + package="org.y20k.trackbook"> - + + @@ -18,27 +19,19 @@ - - - + android:supportsRtl="true" + android:theme="@style/AppTheme" + tools:ignore="GoogleAppIndexingWarning"> + + - - + + - @@ -53,17 +46,20 @@ - + + android:name="androidx.core.content.FileProvider" + android:authorities="${applicationId}.provider" + android:exported="false" + android:grantUriPermissions="true"> + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/provider_paths"/> - \ No newline at end of file + + + + diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png index 6e0beb427ebc942791d475216429acbb1f4b3792..dbd5712eb500a6239c370136bbc94a90e4a85bad 100644 GIT binary patch literal 25989 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%0D^u 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 LtxpsmbBq2DnzIC# literal 26942 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 diff --git a/app/src/main/java/org/y20k/trackbook/Keys.kt b/app/src/main/java/org/y20k/trackbook/Keys.kt new file mode 100644 index 0000000..99702f2 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/Keys.kt @@ -0,0 +1,134 @@ +/* + * Keys.kt + * Implements the keys used throughout the app + * This object hosts all keys used to control Trackbook's state + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook + +import java.util.* + + +/* + * Keys object + */ +object Keys { + + // application name + const val APPLICATION_NAME: String = "Trackbook" + + // version numbers + const val CURRENT_TRACK_FORMAT_VERSION: Int = 4 + const val CURRENT_TRACKLIST_FORMAT_VERSION: Int = 0 + + // other values + const val MAXIMUM_TRACK_FILES: Int = 25 + const val FIFTY_METER_RADIUS: Int = 50 + const val UNIT_METRIC: Int = 1 + const val UNIT_IMPERIAL: Int = -1 + + // intent actions + const val ACTION_START: String = "org.y20k.trackbooks.action.START" + const val ACTION_STOP: String = "org.y20k.trackbooks.action.STOP" + const val ACTION_RESUME: String = "org.y20k.transistors.action.RESUME" + + // args + const val ARG_TRACK_TITLE: String = "ArgTrackTitle" + const val ARG_TRACK_FILE_URI: String = "ArgTrackFileUri" + const val ARG_GPX_FILE_URI: String = "ArgGpxFileUri" + const val ARG_TRACK_ID: String = "ArgTrackId" + + // preferences + const val PREF_ONE_TIME_HOUSEKEEPING_NECESSARY = "ONE_TIME_HOUSEKEEPING_NECESSARY_VERSIONCODE_37" // increment to current app version code to trigger housekeeping that runs only once + const val PREF_NIGHT_MODE_STATE: String= "prefNightModeState" + const val PREF_CURRENT_BEST_LOCATION_PROVIDER: String = "prefCurrentBestLocationProvider" + const val PREF_CURRENT_BEST_LOCATION_LATITUDE: String = "prefCurrentBestLocationLatitude" + const val PREF_CURRENT_BEST_LOCATION_LONGITUDE: String = "prefCurrentBestLocationLongitude" + const val PREF_CURRENT_BEST_LOCATION_ACCURACY: String = "prefCurrentBestLocationAccuracy" + const val PREF_CURRENT_BEST_LOCATION_ALTITUDE: String = "prefCurrentBestLocationAltitude" + const val PREF_CURRENT_BEST_LOCATION_TIME: String = "prefCurrentBestLocationTime" + const val PREF_MAP_ZOOM_LEVEL: String = "prefMapZoomLevel" + const val PREF_TRACKING_STATE: String = "prefTrackingState" + const val PREF_USE_IMPERIAL_UNITS: String = "prefUseImperialUnits" + const val PREF_GPS_ONLY: String = "prefGpsOnly" + const val PREF_LOCATION_ACCURACY_THRESHOLD: String = "prefLocationAccuracyThreshold" + const val PREF_LOCATION_AGE_THRESHOLD: String = "prefLocationAgeThreshold" + + // states + const val STATE_NOT_TRACKING: Int = 0 + const val STATE_TRACKING_ACTIVE: Int = 1 + const val STATE_TRACKING_STOPPED: Int = 2 + + // dialog types + const val DIALOG_EMPTY_RECORDING: Int = 0 + const val DIALOG_REMOVE_TRACK: Int = 1 + + // dialog results + const val DIALOG_RESULT_DEFAULT: Int = -1 + const val DIALOG_EMPTY_PAYLOAD_STRING: String = "" + const val DIALOG_EMPTY_PAYLOAD_INT: Int = -1 + const val DIALOG_RESULT_SAVE_DIALOG: Int = 1 + const val DIALOG_RESULT_CLEAR_DIALOG: Int = 2 + const val DIALOG_RESULT_DELETE_DIALOG: Int = 3 + const val DIALOG_RESULT_EXPORT_DIALOG: Int = 4 + const val DIALOG_RESULT_EMPTY_RECORDING_DIALOG: Int = 5 + + // folder names + const val FOLDER_TEMP: String = "temp" + const val FOLDER_TRACKS: String = "tracks" + const val FOLDER_GPX: String = "gpx" + + // file names and extensions + const val GPX_FILE_EXTENSION: String = ".gpx" + const val TRACKBOOK_LEGACY_FILE_EXTENSION: String = ".trackbook" + const val TRACKBOOK_FILE_EXTENSION: String = ".json" + const val TEMP_FILE: String = "temp.json" + const val TRACKLIST_FILE: String = "tracklist.json" + const val PODCAST_COVER_FILE: String = "cover.jpg" + const val PODCAST_SMALL_COVER_FILE: String = "cover-small.jpg" + const val DEBUG_LOG_FILE: String = "log-can-be-deleted.txt" + const val FILE_TYPE_TEMP: Int = 0 + const val FILE_TYPE_TRACK: Int = 1 + + + // default values + val DEFAULT_DATE: Date = Date(0L) + const val DEFAULT_RFC2822_DATE: String = "Thu, 01 Jan 1970 01:00:00 +0100" // --> Date(0) + const val ONE_HOUR_IN_MILLISECONDS: Int = 3600000 + const val EMPTY_STRING_RESOURCE: Int = 0 + const val REQUEST_CURRENT_LOCATION_INTERVAL: Long = 1000L // 1 second in milliseconds + const val ADD_WAYPOINT_TO_TRACK_INTERVAL: Long = 15000L // 15 seconds in milliseconds + const val SIGNIFICANT_TIME_DIFFERENCE: Long = 120000L // 2 minutes in milliseconds + const val STOP_OVER_THRESHOLD: Long = 300000L // 5 minutes in milliseconds + const val DEFAULT_LATITUDE: Double = 71.172500 // latitude Nordkapp, Norway + const val DEFAULT_LONGITUDE: Double = 25.784444 // longitude Nordkapp, Norway + const val DEFAULT_ACCURACY: Float = 300f // in meters + const val DEFAULT_ALTITUDE: Double = 0.0 + const val DEFAULT_TIME: Long = 0L + const val DEFAULT_THRESHOLD_LOCATION_ACCURACY: Int = 30 // 30 meters + const val DEFAULT_THRESHOLD_LOCATION_AGE: Long = 60000000000L // one minute in nanoseconds + const val DEFAULT_THRESHOLD_DISTANCE: Float = 15f // 15 meters + const val DEFAULT_ZOOM_LEVEL: Double = 16.0 + const val ALTITUDE_MEASUREMENT_ERROR_THRESHOLD = 10 // altitude changes of 10 meter or more (per 15 seconds) are being discarded + const val REQUEST_CODE_FOREGROUND = 42 + + // requests + + // results + + // notification + const val TRACKER_SERVICE_NOTIFICATION_ID: Int = 1 + const val NOTIFICATION_CHANNEL_RECORDING: String = "notificationChannelIdRecordingChannel" + +} diff --git a/app/src/main/java/org/y20k/trackbook/MainActivity.java b/app/src/main/java/org/y20k/trackbook/MainActivity.java deleted file mode 100755 index f8b1659..0000000 --- a/app/src/main/java/org/y20k/trackbook/MainActivity.java +++ /dev/null @@ -1,925 +0,0 @@ -/** - * MainActivity.java - * Implements the app's main activity - * The main activity sets up the main view - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook; - -import android.Manifest; -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.location.Location; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.os.IBinder; -import android.os.Vibrator; -import android.preference.PreferenceManager; -import android.util.SparseArray; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.cardview.widget.CardView; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentStatePagerAdapter; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import com.google.android.material.bottomnavigation.BottomNavigationView; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.snackbar.Snackbar; - -import org.osmdroid.config.Configuration; -import org.y20k.trackbook.helpers.DialogHelper; -import org.y20k.trackbook.helpers.ExportHelper; -import org.y20k.trackbook.helpers.LogHelper; -import org.y20k.trackbook.helpers.NightModeHelper; -import org.y20k.trackbook.helpers.TrackbookKeys; -import org.y20k.trackbook.layout.NonSwipeableViewPager; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - - -/** - * MainActivity class - */ -public class MainActivity extends AppCompatActivity implements TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = MainActivity.class.getSimpleName(); - - - /* Main class variables */ - private TrackerService mTrackerService; - private BottomNavigationView mBottomNavigationView; - private NonSwipeableViewPager mViewPager; - private SectionsPagerAdapter mSectionsPagerAdapter; - private boolean mTrackerServiceRunning; - private boolean mPermissionsGranted; - private boolean mFloatingActionButtonSubMenuVisible; - private List mMissingPermissions; - private FloatingActionButton mFloatingActionButtonMain; - private FloatingActionButton mFloatingActionButtonSubSave; - private FloatingActionButton mFloatingActionButtonSubClear; - private FloatingActionButton mFloatingActionButtonSubResume; - private FloatingActionButton mFloatingActionButtonLocation; - private CardView mFloatingActionButtonSubSaveLabel; - private CardView mFloatingActionButtonSubClearLabel; - private CardView mFloatingActionButtonSubResumeLabel; - private BroadcastReceiver mTrackingChangedReceiver; - private int mFloatingActionButtonState; - private int mSelectedTab; - - private boolean mBound = false; - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // check state of External Storage - checkExternalStorageState(); - - // empty cache - ExportHelper.emptyCacheDirectory(this); - - // load saved state of app - loadFloatingActionButtonState(this); - - // check permissions on Android 6 and higher - mPermissionsGranted = false; - if (Build.VERSION.SDK_INT >= 23) { - // check permissions - mMissingPermissions = checkPermissions(); - mPermissionsGranted = mMissingPermissions.size() == 0; - } else { - mPermissionsGranted = true; - } - - // initialize state - if (savedInstanceState != null) { - // restore if saved instance is available - mTrackerServiceRunning = savedInstanceState.getBoolean(INSTANCE_TRACKING_STATE, false); - mSelectedTab = savedInstanceState.getInt(INSTANCE_SELECTED_TAB, FRAGMENT_ID_MAP); - mFloatingActionButtonSubMenuVisible = savedInstanceState.getBoolean(INSTANCE_FAB_SUB_MENU_VISIBLE, false); - } else { - // use default values - mTrackerServiceRunning = false; - mSelectedTab = FRAGMENT_ID_MAP; - mFloatingActionButtonSubMenuVisible = false; - } - - // set user agent to prevent getting banned from the osm servers - Configuration.getInstance().setUserAgentValue(BuildConfig.APPLICATION_ID); - // set the path for osmdroid's files (e.g. tile cache) - Configuration.getInstance().setOsmdroidBasePath(this.getExternalFilesDir(null)); - - // set up main layout - setupLayout(); - } - - - @Override - protected void onStart() { - super.onStart(); - - // bind to TrackerService - Intent intent = new Intent(this, TrackerService.class); - bindService(intent, mConnection, Context.BIND_AUTO_CREATE); - - // register broadcast receiver for changed tracking state - mTrackingChangedReceiver = createTrackingChangedReceiver(); - IntentFilter trackingStoppedIntentFilter = new IntentFilter(ACTION_TRACKING_STATE_CHANGED); - LocalBroadcastManager.getInstance(this).registerReceiver(mTrackingChangedReceiver, trackingStoppedIntentFilter); - } - - - @Override - protected void onResume() { - super.onResume(); - - // load state of Floating Action Button - loadFloatingActionButtonState(this); - - // handle incoming intent (from notification) - handleIncomingIntent(); - - // if not in onboarding mode: set state of FloatingActionButton - if (mFloatingActionButtonMain != null) { - setFloatingActionButtonState(); - } - } - - - @Override - protected void onPause() { - super.onPause(); - - } - - - @Override - protected void onStop() { - super.onStop(); - // unbind from TrackerService - unbindService(mConnection); - } - - - @Override - public void onDestroy() { - super.onDestroy(); - LogHelper.v(LOG_TAG, "onDestroy called."); - - // reset selected tab - mSelectedTab = FRAGMENT_ID_MAP; - - // disable broadcast receiver - LocalBroadcastManager.getInstance(this).unregisterReceiver(mTrackingChangedReceiver); - } - - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - switch (requestCode) { - case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS: { - Map perms = new HashMap<>(); - perms.put(Manifest.permission.ACCESS_FINE_LOCATION, PackageManager.PERMISSION_GRANTED); - perms.put(Manifest.permission.WRITE_EXTERNAL_STORAGE, PackageManager.PERMISSION_GRANTED); - for (int i = 0; i < permissions.length; i++) - perms.put(permissions[i], grantResults[i]); - - // check for ACCESS_FINE_LOCATION and WRITE_EXTERNAL_STORAGE - Boolean location = perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; - // Boolean storage = perms.get(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; - - if (location) { - // permissions granted - notify user - Toast.makeText(this, R.string.toast_message_permissions_granted, Toast.LENGTH_SHORT).show(); - mPermissionsGranted = true; - // switch to main map layout - setupLayout(); - } else { - // permissions denied - notify user - Toast.makeText(this, R.string.toast_message_unable_to_start_app, Toast.LENGTH_SHORT).show(); - mPermissionsGranted = false; - } - } - break; - default: - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - } - - - @Override - protected void onSaveInstanceState(Bundle outState) { - outState.putBoolean(INSTANCE_TRACKING_STATE, mTrackerServiceRunning); - outState.putInt(INSTANCE_SELECTED_TAB, mSelectedTab); - outState.putBoolean(INSTANCE_FAB_SUB_MENU_VISIBLE, mFloatingActionButtonSubMenuVisible); - super.onSaveInstanceState(outState); - } - - - /* Handles FloatingActionButton dialog results - called by MainActivityMapFragment after Saving and/or clearing the map */ - public void onFloatingActionButtonResult(int requestCode, int resultCode) { - switch(requestCode) { - case RESULT_SAVE_DIALOG: - if (resultCode == Activity.RESULT_OK) { - // user chose SAVE - handleStateAfterSave(); - LogHelper.v(LOG_TAG, "Save dialog result: SAVE"); - } else if (resultCode == Activity.RESULT_CANCELED){ - LogHelper.v(LOG_TAG, "Save dialog result: CANCEL"); - } - break; - case RESULT_CLEAR_DIALOG: - if (resultCode == Activity.RESULT_OK) { - // user chose CLEAR - handleStateAfterClear(); - LogHelper.v(LOG_TAG, "Clear map dialog result: CLEAR"); - } else if (resultCode == Activity.RESULT_CANCELED){ - LogHelper.v(LOG_TAG, "Clear map dialog result: User chose CANCEL."); - } - break; - case RESULT_EMPTY_RECORDING_DIALOG: - if (resultCode == Activity.RESULT_OK) { - // User chose RESUME RECORDING - handleResumeButtonClick((View)mFloatingActionButtonMain); - LogHelper.v(LOG_TAG, "Empty recording dialog result: RESUME"); - } else if (resultCode == Activity.RESULT_CANCELED){ - // User chose CANCEL - do nothing just hide the sub menu - showFloatingActionButtonMenu(false); - LogHelper.v(LOG_TAG, "Empty recording dialog result: CANCEL"); - } - break; - } - } - - - /* Handles the visual state after a save action */ - private void handleStateAfterSave() { - // display and update tracks tab - mBottomNavigationView.setSelectedItemId(R.id.navigation_last_tracks); - - // dismiss notification - dismissNotification(); - - // hide Floating Action Button sub menu - showFloatingActionButtonMenu(false); - - // update Floating Action Button icon - mFloatingActionButtonState = FAB_STATE_DEFAULT; - setFloatingActionButtonState(); - } - - - /* Start tracker service */ - private void startTrackerService() { - // start service so that it keeps running after unbind - Intent intent = new Intent(this, TrackerService.class); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // ... start service in foreground to prevent it being killed on Oreo - startForegroundService(intent); - } else { - startService(intent); - } - } - - - /* Start recording movements */ - private void startRecording(Location lastLocation) { - startTrackerService(); - if (mBound) { - mTrackerService.startTracking(lastLocation); - } - } - - - /* Resume recording movements */ - private void resumeRecording(Location lastLocation) { - startTrackerService(); - if (mBound) { - mTrackerService.resumeTracking(lastLocation); - } - } - - - /* Stop recording movements */ - private void stopRecording() { - if (mBound) { - mTrackerService.stopTracking(); - } - } - - - /* Dismiss notification */ - private void dismissNotification() { - if (mBound) { - mTrackerService.dismissNotification(); - } - } - - - /* Handles the visual state after a save action */ - private void handleStateAfterClear() { - // dismiss notification - dismissNotification(); - - // hide Floating Action Button sub menu - showFloatingActionButtonMenu(false); - - // update Floating Action Button icon - mFloatingActionButtonState = FAB_STATE_DEFAULT; - setFloatingActionButtonState(); - } - - - /* Handles tap on the button "save" */ - private void handleSaveButtonClick() { - // save button click is handled by onActivityResult in MainActivityMapFragment - MainActivityMapFragment mainActivityMapFragment = (MainActivityMapFragment) mSectionsPagerAdapter.getFragment(FRAGMENT_ID_MAP); - mainActivityMapFragment.onActivityResult(RESULT_SAVE_DIALOG, Activity.RESULT_OK, getIntent()); - } - - - /* Handles tap on the button "clear" */ - private void handleClearButtonClick() { - // prepare delete dialog - int dialogTitle = -1; - String dialogMessage = getString(R.string.dialog_clear_content); - int dialogPositiveButton = R.string.dialog_clear_action_clear; - int dialogNegativeButton = R.string.dialog_default_action_cancel; - // show delete dialog - MainActivityMapFragment mainActivityMapFragment = (MainActivityMapFragment) mSectionsPagerAdapter.getFragment(FRAGMENT_ID_MAP); - DialogFragment dialogFragment = DialogHelper.newInstance(dialogTitle, dialogMessage, dialogPositiveButton, dialogNegativeButton); - dialogFragment.setTargetFragment(mainActivityMapFragment, RESULT_CLEAR_DIALOG); - dialogFragment.show(getSupportFragmentManager(), "ClearDialog"); - // results of dialog are handled by onActivityResult in MainActivityMapFragment - } - - - /* Handles tap on the button "resume" */ - private void handleResumeButtonClick(View view) { - - // get last location from MainActivity Fragment // todo check -> may produce NullPointerException - MainActivityMapFragment mainActivityMapFragment = (MainActivityMapFragment) mSectionsPagerAdapter.getFragment(FRAGMENT_ID_MAP); - Location lastLocation = mainActivityMapFragment.getCurrentBestLocation(); - - if (lastLocation != null) { - // show snackbar - Snackbar.make(view, R.string.snackbar_message_tracking_resumed, Snackbar.LENGTH_SHORT).setAction("Action", null).show(); - // resume tracking - resumeRecording(lastLocation); - // hide sub menu - showFloatingActionButtonMenu(false); - } else { - Toast.makeText(this, getString(R.string.toast_message_location_services_not_ready), Toast.LENGTH_LONG).show(); - } - } - - - /* Loads state of Floating Action Button from preferences */ - private void loadFloatingActionButtonState(Context context) { - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); - mFloatingActionButtonState = settings.getInt(PREFS_FAB_STATE, FAB_STATE_DEFAULT); - } - - - /* Set up main layout */ - private void setupLayout() { - if (mPermissionsGranted) { - // point to the main map layout - setContentView(R.layout.activity_main); - - // create adapter that returns fragments for the maim map and the last track display - mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager()); - - // set up the ViewPager with the sections adapter. - mViewPager = (NonSwipeableViewPager) findViewById(R.id.fragmentContainer); - mViewPager.setAdapter(mSectionsPagerAdapter); - - // setup bottom navigation - mBottomNavigationView = findViewById(R.id.navigation); - mBottomNavigationView.setOnNavigationItemSelectedListener(getOnNavigationItemSelectedListener()); - - // get references to the record button and show/hide its sub menu - mFloatingActionButtonMain = findViewById(R.id.fabMainButton); - mFloatingActionButtonLocation = findViewById(R.id.fabLocationButton); - mFloatingActionButtonSubSave = findViewById(R.id.fabSubMenuButtonSave); - mFloatingActionButtonSubSaveLabel = findViewById(R.id.fabSubMenuLabelSave); - mFloatingActionButtonSubClear = findViewById(R.id.fabSubMenuButtonClear); - mFloatingActionButtonSubClearLabel = findViewById(R.id.fabSubMenuLabelClear); - mFloatingActionButtonSubResume = findViewById(R.id.fabSubMenuButtonResume); - mFloatingActionButtonSubResumeLabel = findViewById(R.id.fabSubMenuLabelResume); - if (mFloatingActionButtonSubMenuVisible) { - showFloatingActionButtonMenu(true); - } else { - showFloatingActionButtonMenu(false); - } - - // restore selected tab - if (mSelectedTab == FRAGMENT_ID_TRACKS) { - mBottomNavigationView.setSelectedItemId(R.id.navigation_last_tracks); - } else { - mBottomNavigationView.setSelectedItemId(R.id.navigation_map); - } - - // add listeners to buttons - addListenersToViews(); - - } else { - // point to the on main onboarding layout - setContentView(R.layout.main_onboarding); - - // show the okay button and attach listener - Button okayButton = (Button) findViewById(R.id.button_okay); - okayButton.setOnClickListener(new View.OnClickListener() { - @TargetApi(Build.VERSION_CODES.M) - @Override - public void onClick(View view) { - if (mMissingPermissions != null && !mMissingPermissions.isEmpty()) { - // request permissions - String[] params = mMissingPermissions.toArray(new String[mMissingPermissions.size()]); - requestPermissions(params, REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS); - } - } - }); - - } - - } - - - /* Add listeners to ui buttons */ - private void addListenersToViews() { - - mFloatingActionButtonMain.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - handleFloatingActionButtonClick(view); - } - }); - mFloatingActionButtonSubSave.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - handleSaveButtonClick(); - } - }); - mFloatingActionButtonSubSaveLabel.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - handleSaveButtonClick(); - } - }); - mFloatingActionButtonSubClear.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - handleClearButtonClick(); - } - }); - mFloatingActionButtonSubClearLabel.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - handleClearButtonClick(); - } - }); - mFloatingActionButtonSubResume.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - handleResumeButtonClick(view); - } - }); - mFloatingActionButtonSubResumeLabel.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - handleResumeButtonClick(view); - } - }); - - - mFloatingActionButtonLocation.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - MainActivityMapFragment mainActivityMapFragment = (MainActivityMapFragment) mSectionsPagerAdapter.getFragment(FRAGMENT_ID_MAP); - mainActivityMapFragment.handleShowMyLocation(); - } - }); - - // secret night mode switch - mFloatingActionButtonLocation.setOnLongClickListener(new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - NightModeHelper.switchMode(MainActivity.this); - // vibrate 50 milliseconds - Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); - vibrator.vibrate(50); - // recreate activity - recreate(); - return true; - } - }); - } - - - /* Handles tap on the record button */ - private void handleFloatingActionButtonClick(View view) { - - switch (mFloatingActionButtonState) { - case FAB_STATE_DEFAULT: - - // get last location from MainActivity Fragment // todo check -> may produce NullPointerException - MainActivityMapFragment mainActivityMapFragment = (MainActivityMapFragment) mSectionsPagerAdapter.getFragment(FRAGMENT_ID_MAP); - Location lastLocation = mainActivityMapFragment.getCurrentBestLocation(); - - if (lastLocation != null) { - // show snackbar - Snackbar.make(view, R.string.snackbar_message_tracking_started, Snackbar.LENGTH_SHORT).setAction("Action", null).show(); - // start recording - startRecording(lastLocation); - } else { - Toast.makeText(this, getString(R.string.toast_message_location_services_not_ready), Toast.LENGTH_LONG).show(); - } - - break; - - case FAB_STATE_RECORDING: - // show snackbar - Snackbar.make(view, R.string.snackbar_message_tracking_stopped, Snackbar.LENGTH_SHORT).setAction("Action", null).show(); - // stop tracker service - stopRecording(); - - break; - - case FAB_STATE_SAVE: - // toggle visibility floating action button sub menu - showFloatingActionButtonMenu(!mFloatingActionButtonSubMenuVisible); - - break; - - } - - // update tracking state in MainActivityMapFragment // todo check -> may produce NullPointerException - MainActivityMapFragment mainActivityMapFragment = (MainActivityMapFragment) mSectionsPagerAdapter.getFragment(FRAGMENT_ID_MAP); - mainActivityMapFragment.setTrackingState(mTrackerServiceRunning); - } - - - /* Set state of FloatingActionButton */ - private void setFloatingActionButtonState() { - - switch (mFloatingActionButtonState) { - case FAB_STATE_DEFAULT: - mFloatingActionButtonMain.hide(); // workaround todo remove asap - mFloatingActionButtonMain.setImageResource(R.drawable.ic_fiber_manual_record_white_24dp); - mFloatingActionButtonMain.setContentDescription(getString(R.string.descr_fab_main_start)); - if (mSelectedTab == FRAGMENT_ID_MAP) mFloatingActionButtonMain.show(); // workaround todo remove asap - break; - case FAB_STATE_RECORDING: - mFloatingActionButtonMain.hide(); // workaround todo remove asap - mFloatingActionButtonMain.setImageResource(R.drawable.ic_fiber_manual_record_red_24dp); - mFloatingActionButtonMain.setContentDescription(getString(R.string.descr_fab_main_stop)); - if (mSelectedTab == FRAGMENT_ID_MAP) mFloatingActionButtonMain.show(); // workaround todo remove asap - break; - case FAB_STATE_SAVE: - mFloatingActionButtonMain.hide(); // workaround todo remove asap - mFloatingActionButtonMain.setImageResource(R.drawable.ic_save_white_24dp); - mFloatingActionButtonMain.setContentDescription(getString(R.string.descr_fab_main_options)); - if (mSelectedTab == FRAGMENT_ID_MAP) mFloatingActionButtonMain.show(); // workaround todo remove asap - break; - default: - mFloatingActionButtonMain.hide(); // workaround todo remove asap - mFloatingActionButtonMain.setImageResource(R.drawable.ic_fiber_manual_record_white_24dp); - mFloatingActionButtonMain.setContentDescription(getString(R.string.descr_fab_main_start)); - if (mSelectedTab == FRAGMENT_ID_MAP) mFloatingActionButtonMain.show(); // workaround todo remove asap - break; - } - } - - - /* Shows (and hides) the sub menu of the floating action button */ - private void showFloatingActionButtonMenu(boolean visible) { - if (visible) { - mFloatingActionButtonSubResume.show(); - mFloatingActionButtonSubResumeLabel.setVisibility(View.VISIBLE); - mFloatingActionButtonSubClear.show(); - mFloatingActionButtonSubClearLabel.setVisibility(View.VISIBLE); - mFloatingActionButtonSubSave.show(); - mFloatingActionButtonSubSaveLabel.setVisibility(View.VISIBLE); - mFloatingActionButtonSubMenuVisible = true; - } else { - mFloatingActionButtonSubResume.hide(); - mFloatingActionButtonSubResumeLabel.setVisibility(View.INVISIBLE); - mFloatingActionButtonSubClear.hide(); - mFloatingActionButtonSubClearLabel.setVisibility(View.INVISIBLE); - mFloatingActionButtonSubSave.hide(); - mFloatingActionButtonSubSaveLabel.setVisibility(View.INVISIBLE); - mFloatingActionButtonSubMenuVisible = false; - } - } - - - /* Handles taps on the bottom navigation */ - private BottomNavigationView.OnNavigationItemSelectedListener getOnNavigationItemSelectedListener() { - return new BottomNavigationView.OnNavigationItemSelectedListener() { - @Override - public boolean onNavigationItemSelected(@NonNull MenuItem item) { - switch (item.getItemId()) { - case R.id.navigation_map: - // show the Floating Action Button - mFloatingActionButtonMain.show(); - - // show the my location button - mFloatingActionButtonLocation.show(); - - // show map fragment - mSelectedTab = FRAGMENT_ID_MAP; - mViewPager.setCurrentItem(mSelectedTab); - - return true; - - case R.id.navigation_last_tracks: - // hide the Floating Action Button - and its sub menu - mFloatingActionButtonMain.hide(); - showFloatingActionButtonMenu(false); - - // hide the my location button - mFloatingActionButtonLocation.hide(); - - // show tracks fragment - mSelectedTab = FRAGMENT_ID_TRACKS; - mViewPager.setCurrentItem(mSelectedTab); - - return true; - - default: - // show the Floating Action Button - mFloatingActionButtonMain.show(); - return false; - } - } - }; - } - - - /* Handles new incoming intents */ - private void handleIncomingIntent() { - Intent intent = getIntent(); - LogHelper.v(LOG_TAG, "Main Activity received intent. Content: " + intent.toString()); - String intentAction = intent.getAction(); - switch (intentAction) { - case ACTION_SHOW_MAP: - // show map fragment - mBottomNavigationView.setSelectedItemId(R.id.navigation_map); - - // clear intent - intent.setAction(ACTION_DEFAULT); - - break; - - case ACTION_CLEAR: - // show map fragment - mBottomNavigationView.setSelectedItemId(R.id.navigation_map); - - // show clear dialog - handleClearButtonClick(); - - // clear intent - intent.setAction(ACTION_DEFAULT); - - break; - - default: - break; - } - } - - - /* Inform user and give haptic feedback (vibration) */ - private void longPressFeedback(int stringResource) { - // inform user - Toast.makeText(this, stringResource, Toast.LENGTH_LONG).show(); - // vibrate 50 milliseconds - Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); - if (v != null) { - v.vibrate(50); -// v.vibrate(VibrationEffect.createOneShot(50, DEFAULT_AMPLITUDE)); // todo check if there is a support library vibrator - } - } - - - - /* Check which permissions have been granted */ - private List checkPermissions() { - List permissions = new ArrayList<>(); - - // check for location permission - if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { - // add missing permission - permissions.add(Manifest.permission.ACCESS_FINE_LOCATION); - } - -// // check for storage permission -// if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { -// // add missing permission -// permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); -// } - - return permissions; - } - - - /* Creates receiver for stopped tracking */ - private BroadcastReceiver createTrackingChangedReceiver() { - return new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - - // change state - mTrackerServiceRunning = intent.getBooleanExtra(EXTRA_TRACKING_STATE, false); - if (mTrackerServiceRunning) { - mFloatingActionButtonState = FAB_STATE_RECORDING; - } else { - mFloatingActionButtonState = FAB_STATE_SAVE; - } - setFloatingActionButtonState(); - - // pass tracking state to MainActivityMapFragment // todo check -> may produce NullPointerException - MainActivityMapFragment mainActivityMapFragment = (MainActivityMapFragment) mSectionsPagerAdapter.getFragment(FRAGMENT_ID_MAP); - mainActivityMapFragment.setTrackingState(mTrackerServiceRunning); - } - }; - } - - - /* Checks the state of External Storage */ - private void checkExternalStorageState() { - - String state = Environment.getExternalStorageState(); - if (!state.equals(Environment.MEDIA_MOUNTED)) { - LogHelper.e(LOG_TAG, "Error: Unable to mount External Storage. Current state: " + state); - - // move MainActivity to back - moveTaskToBack(true); - - // shutting down app - android.os.Process.killProcess(android.os.Process.myPid()); - System.exit(1); - } - } - - -// public class SectionsPagerAdapter extends FragmentPagerAdapter { -// -// public SectionsPagerAdapter(FragmentManager fm) { -// super(fm); -// } -// -// @Override -// public Fragment getItem(int position) { -// // getItem is called to instantiate the fragment for the given page. -// switch (position) { -// case FRAGMENT_ID_MAP: -// return new MainActivityMapFragment(); -// case FRAGMENT_ID_TRACKS: -// return new MainActivityTrackFragment(); -// } -// return null; -// } -// -// @Override -// public int getCount() { -// return 2; -// } -// -// public Fragment getFragment(int pos) { -// return getItem(pos); -// } -// -// } - - - /** - * Defines callbacks for service binding, passed to bindService() - */ - private ServiceConnection mConnection = new ServiceConnection() { - - @Override - public void onServiceConnected(ComponentName className, IBinder service) { - // We've bound to LocalService, cast the IBinder and get LocalService instance - TrackerService.LocalBinder binder = (TrackerService.LocalBinder) service; - mTrackerService = binder.getService(); - mBound = true; - } - - @Override - public void onServiceDisconnected(ComponentName arg0) { - mBound = false; - } - }; - - - /** - * Inner class: SectionsPagerAdapter that returns a fragment corresponding to one of the tabs. - * see also: https://developer.android.com/reference/android/support/v4/app/FragmentPagerAdapter.html - * and: http://www.truiton.com/2015/12/android-activity-fragment-communication/ - */ - public class SectionsPagerAdapter extends FragmentStatePagerAdapter { - - private final SparseArray> instantiatedFragments = new SparseArray<>(); - - public SectionsPagerAdapter(FragmentManager fm) { - super(fm); - } - - @Override - public Fragment getItem(int position) { - // getItem is called to instantiate the fragment for the given page. - switch (position) { - case FRAGMENT_ID_MAP: - return new MainActivityMapFragment(); - case FRAGMENT_ID_TRACKS: - return new MainActivityTrackFragment(); - } - return null; - } - - @Override - public int getCount() { - // show 2 total pages. - return 2; - } - - @Override - public CharSequence getPageTitle(int position) { - switch (position) { - case FRAGMENT_ID_MAP: - return getString(R.string.tab_map); - case FRAGMENT_ID_TRACKS: - return getString(R.string.tab_last_tracks); - } - return null; - } - - @NonNull - @Override - public Object instantiateItem(final ViewGroup container, final int position) { - final Fragment fragment = (Fragment) super.instantiateItem(container, position); - instantiatedFragments.put(position, new WeakReference<>(fragment)); - return fragment; - } - - @Override - public void destroyItem(final ViewGroup container, final int position, final Object object) { - instantiatedFragments.remove(position); - super.destroyItem(container, position, object); - } - - @Nullable - public Fragment getFragment(final int position) { - final WeakReference wr = instantiatedFragments.get(position); - if (wr != null) { - return wr.get(); - } else { - return null; - } - } - - } - /** - * End of inner class - */ - - -} diff --git a/app/src/main/java/org/y20k/trackbook/MainActivity.kt b/app/src/main/java/org/y20k/trackbook/MainActivity.kt new file mode 100644 index 0000000..d341827 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/MainActivity.kt @@ -0,0 +1,85 @@ +/* + * MainActivity.kt + * Implements the main activity of the app + * The MainActivity hosts fragments for: current map, track list, settings + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.setupWithNavController +import com.google.android.material.bottomnavigation.BottomNavigationView +import org.osmdroid.config.Configuration +import org.y20k.trackbook.helpers.ImportHelper +import org.y20k.trackbook.helpers.LogHelper +import org.y20k.trackbook.helpers.PreferencesHelper + + +/* + * MainActivity class + */ +class MainActivity : AppCompatActivity() { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(MainActivity::class.java) + + + /* Main class variables */ + private lateinit var navHostFragment: NavHostFragment + private lateinit var bottomNavigationView: BottomNavigationView + + + /* Overrides onCreate from AppCompatActivity */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // set user agent to prevent getting banned from the osm servers + Configuration.getInstance().userAgentValue = BuildConfig.APPLICATION_ID + // set the path for osmdroid's files (e.g. tile cache) + Configuration.getInstance().osmdroidBasePath = this.getExternalFilesDir(null) + + // set up views + setContentView(R.layout.activity_main) + navHostFragment = supportFragmentManager.findFragmentById(R.id.main_container) as NavHostFragment + bottomNavigationView = findViewById(R.id.bottom_navigation_view) + bottomNavigationView.setupWithNavController(navController = navHostFragment.navController) + + // listen for navigation changes + navHostFragment.navController.addOnDestinationChangedListener { _, destination, _ -> + when (destination.id) { + R.id.fragment_track -> { + runOnUiThread( Runnable() { + run(){ + // mark menu item "Tracks" as checked + bottomNavigationView.menu.findItem(R.id.tracklist_fragment).setChecked(true) + } + }) + } + else -> { + // do nothing + } + } + } + + // convert old tracks (one-time import) + if (PreferencesHelper.isHouseKeepingNecessary(this)) { + ImportHelper.convertOldTracks(this) + PreferencesHelper.saveHouseKeepingNecessaryState(this) + } + + } + +} diff --git a/app/src/main/java/org/y20k/trackbook/MainActivityMapFragment.java b/app/src/main/java/org/y20k/trackbook/MainActivityMapFragment.java deleted file mode 100755 index ebf6950..0000000 --- a/app/src/main/java/org/y20k/trackbook/MainActivityMapFragment.java +++ /dev/null @@ -1,741 +0,0 @@ -/** - * MainActivityMapFragment.java - * Implements the map fragment used in the map tab of the main activity - * This fragment displays a map using osmdroid - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook; - -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.database.ContentObserver; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Handler; -import android.os.SystemClock; -import android.preference.PreferenceManager; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import com.google.android.material.snackbar.Snackbar; - -import org.osmdroid.api.IMapController; -import org.osmdroid.tileprovider.tilesource.TileSourceFactory; -import org.osmdroid.util.GeoPoint; -import org.osmdroid.views.MapView; -import org.osmdroid.views.overlay.ItemizedIconOverlay; -import org.osmdroid.views.overlay.TilesOverlay; -import org.osmdroid.views.overlay.compass.CompassOverlay; -import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider; -import org.y20k.trackbook.core.Track; -import org.y20k.trackbook.helpers.DialogHelper; -import org.y20k.trackbook.helpers.LocationHelper; -import org.y20k.trackbook.helpers.LogHelper; -import org.y20k.trackbook.helpers.MapHelper; -import org.y20k.trackbook.helpers.NightModeHelper; -import org.y20k.trackbook.helpers.StorageHelper; -import org.y20k.trackbook.helpers.TrackbookKeys; - -import java.util.List; - - -/** - * MainActivityMapFragment class - */ -public class MainActivityMapFragment extends Fragment implements TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = MainActivityMapFragment.class.getSimpleName(); - - - /* Main class variables */ - private Activity mActivity; - private Track mTrack; - private boolean mFirstStart; - private Snackbar mLocationOffBar; - private BroadcastReceiver mTrackUpdatedReceiver; - private SettingsContentObserver mSettingsContentObserver; - private MapView mMapView; - private IMapController mController; - private StorageHelper mStorageHelper; - private LocationManager mLocationManager; - private LocationListener mGPSListener; - private LocationListener mNetworkListener; - private ItemizedIconOverlay mMyLocationOverlay; - private ItemizedIconOverlay mTrackOverlay; - private Location mCurrentBestLocation; - private boolean mTrackerServiceRunning; - private boolean mLocalTrackerRunning; - private boolean mLocationSystemSetting; - private boolean mFragmentVisible; - - - /* Constructor (default) */ - public MainActivityMapFragment() { - } - - - /* Return a new Instance of MainActivityMapFragment */ - public static MainActivityMapFragment newInstance() { - return new MainActivityMapFragment(); - } - - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // get activity - mActivity = getActivity(); - - // restore first start state and tracking state - mFirstStart = true; - mTrackerServiceRunning = false; - loadTrackerServiceState(mActivity); - if (savedInstanceState != null) { - mFirstStart = savedInstanceState.getBoolean(INSTANCE_FIRST_START, true); - } - - // create storage helper - mStorageHelper = new StorageHelper(mActivity); - - // acquire reference to Location Manager - mLocationManager = (LocationManager) mActivity.getSystemService(Context.LOCATION_SERVICE); - - // CASE 1: get saved location if possible - if (savedInstanceState != null) { - Location savedLocation = savedInstanceState.getParcelable(INSTANCE_CURRENT_LOCATION); - // check if saved location is still current - if (LocationHelper.isCurrent(savedLocation)) { - mCurrentBestLocation = savedLocation; - } else { - mCurrentBestLocation = null; - } - } - - // CASE 2: get last known location if no saved location or saved location is too old - if (mCurrentBestLocation == null && mLocationManager.getProviders(true).size() > 0) { - mCurrentBestLocation = LocationHelper.determineLastKnownLocation(mLocationManager); - } - - // CASE 3: location services are available but unable to get location - this should not happen - if (mCurrentBestLocation == null) { - mCurrentBestLocation = new Location(LocationManager.NETWORK_PROVIDER); - mCurrentBestLocation.setLatitude(DEFAULT_LATITUDE); - mCurrentBestLocation.setLongitude(DEFAULT_LONGITUDE); - } - - // get state of location system setting - mLocationSystemSetting = LocationHelper.checkLocationSystemSetting(mActivity); - - // create content observer for changes in System Settings - mSettingsContentObserver = new SettingsContentObserver( new Handler()); - - // register broadcast receiver for new WayPoints - mTrackUpdatedReceiver = createTrackUpdatedReceiver(); - IntentFilter trackUpdatedIntentFilter = new IntentFilter(ACTION_TRACK_UPDATED); - LocalBroadcastManager.getInstance(mActivity).registerReceiver(mTrackUpdatedReceiver, trackUpdatedIntentFilter); - } - - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - // create basic map - mMapView = new MapView(inflater.getContext()); - - // get map controller - mController = mMapView.getController(); - - // basic map setup - mMapView.setTileSource(TileSourceFactory.MAPNIK); - mMapView.setTilesScaledToDpi(true); - - // set dark map tiles, if necessary - if (NightModeHelper.getNightMode(mActivity)) { - mMapView.getOverlayManager().getTilesOverlay().setColorFilter(TilesOverlay.INVERT_COLORS); - } - - // add multi-touch capability - mMapView.setMultiTouchControls(true); - - // disable default zoom controls - mMapView.getZoomController().setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER); - - // add compass to map - CompassOverlay compassOverlay = new CompassOverlay(mActivity, new InternalCompassOrientationProvider(mActivity), mMapView); - compassOverlay.enableCompass(); - mMapView.getOverlays().add(compassOverlay); - - // initiate map state - if (savedInstanceState != null) { - // restore saved instance of map - GeoPoint position = new GeoPoint(savedInstanceState.getDouble(INSTANCE_LATITUDE_MAIN_MAP, DEFAULT_LATITUDE), savedInstanceState.getDouble(INSTANCE_LONGITUDE_MAIN_MAP, DEFAULT_LONGITUDE)); - mController.setCenter(position); - mController.setZoom(savedInstanceState.getDouble(INSTANCE_ZOOM_LEVEL_MAIN_MAP, 16f)); - // restore current location - mCurrentBestLocation = savedInstanceState.getParcelable(INSTANCE_CURRENT_LOCATION); - } else if (mCurrentBestLocation != null) { - // fallback or first run: set map to current position - GeoPoint position = convertToGeoPoint(mCurrentBestLocation); - mController.setCenter(position); - mController.setZoom(16f); - } - - // inform user that new/better location is on its way - if (mFirstStart && !mTrackerServiceRunning) { - Toast.makeText(mActivity, mActivity.getString(R.string.toast_message_acquiring_location), Toast.LENGTH_LONG).show(); - mFirstStart = false; - } - -// // load track from saved instance -// if (savedInstanceState != null) { -// mTrack = savedInstanceState.getParcelable(INSTANCE_TRACK_MAIN_MAP); -// } - - // mark user's location on map - if (mCurrentBestLocation != null && !mTrackerServiceRunning) { - mMyLocationOverlay = MapHelper.createMyLocationOverlay(mActivity, mCurrentBestLocation, LocationHelper.isCurrent(mCurrentBestLocation), false); - mMapView.getOverlays().add(mMyLocationOverlay); - } - - return mMapView; - } - - - @Override - public void onResume() { - super.onResume(); - - // set visibility - mFragmentVisible = true; - - // load state of tracker service - see if anything changed - loadTrackerServiceState(mActivity); - - // load track from temp file if it exists - if (mStorageHelper.tempFileExists()) { - LoadTempTrackAsyncHelper loadTempTrackAsyncHelper = new LoadTempTrackAsyncHelper(); - loadTempTrackAsyncHelper.execute(); - } - -// // CASE 1: recording active -// if (mTrackerServiceRunning) { -// // request an updated track recording from service -// ((MainActivity)mActivity).requestTrack(); -// } -// -// // CASE 2: recording stopped - temp file exists -// else if (mStorageHelper.tempFileExists()) { -// // load track from temp file if it exists -// LoadTempTrackAsyncHelper loadTempTrackAsyncHelper = new LoadTempTrackAsyncHelper(); -// loadTempTrackAsyncHelper.execute(); -// } - -// // CASE 3: not recording and no temp file -// else if (mTrack != null) { -// // just draw existing track data (from saved instance) -// drawTrackOverlay(mTrack); -// } - - // show/hide the location off notification bar - toggleLocationOffBar(); - - // start preliminary tracking - if no TrackerService is running - if (!mTrackerServiceRunning && mFragmentVisible) { - startPreliminaryTracking(); - } - - // register content observer for changes in System Settings - mActivity.getContentResolver().registerContentObserver(android.provider.Settings.Secure.CONTENT_URI, true, mSettingsContentObserver ); - } - - - @Override - public void onPause() { - super.onPause(); - - // set visibility - mFragmentVisible = false; - - // disable preliminary location listeners - stopPreliminaryTracking(); - - // disable content observer for changes in System Settings - mActivity.getContentResolver().unregisterContentObserver(mSettingsContentObserver); - } - - - @Override - public void onDestroyView(){ - super.onDestroyView(); - - // deactivate map - mMapView.onDetach(); - } - - - @Override - public void onDestroy() { - LogHelper.v(LOG_TAG, "onDestroy called."); - - // reset first start state - mFirstStart = true; - - // disable broadcast receivers - LocalBroadcastManager.getInstance(mActivity).unregisterReceiver(mTrackUpdatedReceiver); - - super.onDestroy(); - } - - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - switch(requestCode) { - case RESULT_SAVE_DIALOG: - if (resultCode == Activity.RESULT_OK) { - // user chose SAVE - if (mTrack.getSize() > 0) { - // Track is not empty - clear map AND save track - clearMap(true); - // FloatingActionButton state is already being handled in MainActivity - ((MainActivity)mActivity).onFloatingActionButtonResult(requestCode, resultCode); - LogHelper.v(LOG_TAG, "Save dialog result: SAVE"); - } else { - // track is empty - handleEmptyRecordingSaveRequest(); - } - } else if (resultCode == Activity.RESULT_CANCELED){ - LogHelper.v(LOG_TAG, "Save dialog result: CANCEL"); - } - break; - case RESULT_CLEAR_DIALOG: - if (resultCode == Activity.RESULT_OK) { - // User chose CLEAR - if (mTrack.getSize() > 0) { - // Track is not empty - notify user - Toast.makeText(mActivity, getString(R.string.toast_message_track_clear), Toast.LENGTH_LONG).show(); - } - // clear map, DO NOT save track - clearMap(false); - // handle FloatingActionButton state in MainActivity - ((MainActivity)mActivity).onFloatingActionButtonResult(requestCode, resultCode); - } else if (resultCode == Activity.RESULT_CANCELED){ - LogHelper.v(LOG_TAG, "Clear dialog result: CANCEL"); - } - break; - case RESULT_EMPTY_RECORDING_DIALOG: - // handle FloatingActionButton state and possible Resume-Action in MainActivity - ((MainActivity)mActivity).onFloatingActionButtonResult(requestCode, resultCode); - break; - } - } - - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putBoolean(INSTANCE_FIRST_START, mFirstStart); - outState.putBoolean(INSTANCE_TRACKING_STATE, mTrackerServiceRunning); - outState.putParcelable(INSTANCE_CURRENT_LOCATION, mCurrentBestLocation); - outState.putDouble(INSTANCE_LATITUDE_MAIN_MAP, mMapView.getMapCenter().getLatitude()); - outState.putDouble(INSTANCE_LONGITUDE_MAIN_MAP, mMapView.getMapCenter().getLongitude()); - outState.putDouble(INSTANCE_ZOOM_LEVEL_MAIN_MAP, mMapView.getZoomLevelDouble()); -// outState.putParcelable(INSTANCE_TRACK_MAIN_MAP, mTrack); - super.onSaveInstanceState(outState); - } - - - /* Setter for tracking state */ - public void setTrackingState(boolean trackingState) { - mTrackerServiceRunning = trackingState; - - // turn on/off tracking for MainActivity Fragment - prevent double tracking - if (mTrackerServiceRunning) { - stopPreliminaryTracking(); - } else if (!mLocalTrackerRunning && mFragmentVisible) { - startPreliminaryTracking(); - } - - if (mTrack != null) { - drawTrackOverlay(mTrack); // TODO check if redundant - } - - // update marker - updateMyLocationMarker(); - LogHelper.v(LOG_TAG, "TrackingState: " + trackingState); - } - - - /* Getter for current best location */ - public Location getCurrentBestLocation() { - if (mLocationSystemSetting) { - return mCurrentBestLocation; - } else { - return null; - } - } - - - /* Handles tap on the my location button */ - public boolean handleShowMyLocation() { - - // do nothing if location setting is off - if (toggleLocationOffBar()) { - stopPreliminaryTracking(); - return false; - } - - GeoPoint position; - - // get current position - if (mTrackerServiceRunning && mTrack != null && mTrack.getSize() > 0) { - // get current Location from tracker service - mCurrentBestLocation = mTrack.getWayPointLocation(mTrack.getSize() - 1); - } else if (mCurrentBestLocation == null) { - // app does not have any location fix - mCurrentBestLocation = LocationHelper.determineLastKnownLocation(mLocationManager); - } - - // check if really got a position - if (mCurrentBestLocation != null) { - position = convertToGeoPoint(mCurrentBestLocation); - - // center map on current position - mController.setCenter(position); - - // mark user's new location on map and remove last marker - updateMyLocationMarker(); - - // inform user about location quality - String locationInfo; - long locationAge = (SystemClock.elapsedRealtimeNanos() - mCurrentBestLocation.getElapsedRealtimeNanos()) / 1000000; - String locationAgeString = LocationHelper.convertToReadableTime(locationAge, false); - if (locationAgeString == null) { - locationAgeString = mActivity.getString(R.string.toast_message_last_location_age_one_hour); - } - locationInfo = " " + locationAgeString + " | " + mCurrentBestLocation.getProvider(); - Toast.makeText(mActivity, mActivity.getString(R.string.toast_message_last_location) + locationInfo, Toast.LENGTH_LONG).show(); - return true; - } else { - Toast.makeText(mActivity, mActivity.getString(R.string.toast_message_location_services_not_ready), Toast.LENGTH_LONG).show(); - return false; - } - } - - - /* Removes track crumbs from map */ - private void clearMap(boolean saveTrack) { - - // clear map - if (mTrackOverlay != null) { - mMapView.getOverlays().remove(mTrackOverlay); - mTrackOverlay = null; - } - - if (saveTrack) { - // save track object if requested - SaveTrackAsyncHelper saveTrackAsyncHelper = new SaveTrackAsyncHelper(); - saveTrackAsyncHelper.execute(); - Toast.makeText(mActivity, mActivity.getString(R.string.toast_message_save_track), Toast.LENGTH_LONG).show(); - } else { - // clear track object and delete temp file - mTrack = null; - mStorageHelper.deleteTempFile(); - } - - } - - - /* Handles case when user chose to save recording with zero waypoints */ // todo implement - private void handleEmptyRecordingSaveRequest() { - // prepare empty recording dialog ("Unable to save") - int dialogTitle = R.string.dialog_error_empty_recording_title; - String dialogMessage = getString(R.string.dialog_error_empty_recording_content); - int dialogPositiveButton = R.string.dialog_error_empty_recording_action_resume; - int dialogNegativeButton = R.string.dialog_default_action_cancel; - // show empty recording dialog - DialogFragment dialogFragment = DialogHelper.newInstance(dialogTitle, dialogMessage, dialogPositiveButton, dialogNegativeButton); - dialogFragment.setTargetFragment(this, RESULT_EMPTY_RECORDING_DIALOG); - dialogFragment.show(((AppCompatActivity)mActivity).getSupportFragmentManager(), "EmptyRecordingDialog"); - // results of dialog are handled by onActivityResult - } - - - /* Start preliminary tracking for map */ - private void startPreliminaryTracking() { - if (mLocationSystemSetting && !mLocalTrackerRunning) { - // create location listeners - List locationProviders = mLocationManager.getAllProviders(); - if (locationProviders.contains(LocationManager.GPS_PROVIDER)) { - mGPSListener = createLocationListener(); - mLocalTrackerRunning = true; - } - if (locationProviders.contains(LocationManager.NETWORK_PROVIDER)) { - mNetworkListener = createLocationListener(); - mLocalTrackerRunning = true; - } - // register listeners - LocationHelper.registerLocationListeners(mLocationManager, mGPSListener, mNetworkListener); - LogHelper.v(LOG_TAG, "Starting preliminary tracking."); - } - } - - - /* Removes gps and network location listeners */ - private void stopPreliminaryTracking() { - if (mLocalTrackerRunning) { - mLocalTrackerRunning = false; - // remove listeners - LocationHelper.removeLocationListeners(mLocationManager, mGPSListener, mNetworkListener); - LogHelper.v(LOG_TAG, "Stopping preliminary tracking."); - } - } - - - /* Creates listener for changes in location status */ - private LocationListener createLocationListener() { - return new LocationListener() { - public void onLocationChanged(Location location) { - // check if the new location is better - if (mCurrentBestLocation == null || LocationHelper.isBetterLocation(location, mCurrentBestLocation)) { - // save location - mCurrentBestLocation = location; - // mark user's new location on map and remove last marker - updateMyLocationMarker(); - } - } - - public void onStatusChanged(String provider, int status, Bundle extras) { - LogHelper.v(LOG_TAG, "Location provider status change: " + provider + " | " + status); - } - - public void onProviderEnabled(String provider) { - LogHelper.v(LOG_TAG, "Location provider enabled: " + provider); - } - - public void onProviderDisabled(String provider) { - LogHelper.v(LOG_TAG, "Location provider disabled: " + provider); - } - }; - } - - - /* Updates marker for current user location */ - private void updateMyLocationMarker() { - mMapView.getOverlays().remove(mMyLocationOverlay); - // only update while not tracking - if (!mTrackerServiceRunning) { - mMyLocationOverlay = MapHelper.createMyLocationOverlay(mActivity, mCurrentBestLocation, LocationHelper.isCurrent(mCurrentBestLocation), false); - mMapView.getOverlays().add(mMyLocationOverlay); - } - } - - - /* Draws track onto overlay */ - private void drawTrackOverlay(Track track) { - mMapView.getOverlays().remove(mTrackOverlay); - mTrackOverlay = null; - if (track == null || track.getSize() == 0) { - LogHelper.i(LOG_TAG, "Waiting for a track. Showing preliminary location."); - mTrackOverlay = MapHelper.createMyLocationOverlay(mActivity, mCurrentBestLocation, false, mTrackerServiceRunning); - Toast.makeText(mActivity, mActivity.getString(R.string.toast_message_acquiring_location), Toast.LENGTH_LONG).show(); - } else { - LogHelper.v(LOG_TAG, "Drawing track overlay."); - mTrackOverlay = MapHelper.createTrackOverlay(mActivity, track, mTrackerServiceRunning); - } - mMapView.getOverlays().add(mTrackOverlay); - - } - - - /* Toggles snackbar indicating that location setting is off */ - private boolean toggleLocationOffBar() { - // create snackbar indicator for location setting off - if (mLocationOffBar == null) { - mLocationOffBar = Snackbar.make(mMapView, R.string.snackbar_message_location_offline, Snackbar.LENGTH_INDEFINITE).setAction("Action", null); - } - - // get state of location system setting - mLocationSystemSetting = LocationHelper.checkLocationSystemSetting(mActivity); - - // show snackbar if necessary - if (!mLocationSystemSetting) { - // show snackbar - mLocationOffBar.show(); - return true; - - } else { - // hide snackbar - mLocationOffBar.dismiss(); - return false; - } - - } - - - /* Creates receiver for new WayPoints */ - private BroadcastReceiver createTrackUpdatedReceiver() { - return new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.hasExtra(EXTRA_TRACK) && intent.hasExtra(EXTRA_LAST_LOCATION)) { - // draw track on map - mTrack = intent.getParcelableExtra(EXTRA_TRACK); - drawTrackOverlay(mTrack); - // center map over last location - mCurrentBestLocation = intent.getParcelableExtra(EXTRA_LAST_LOCATION); - mController.setCenter(convertToGeoPoint(mCurrentBestLocation)); - // clear intent - intent.setAction(ACTION_DEFAULT); - } - } - }; - } - - - /* Converts Location to GeoPoint */ - private GeoPoint convertToGeoPoint (Location location) { - if (location != null) { - return new GeoPoint(location.getLatitude(), location.getLongitude()); - } else { - return new GeoPoint(DEFAULT_LATITUDE, DEFAULT_LONGITUDE); - } - } - - - /* Loads state tracker service from preferences */ - private void loadTrackerServiceState(Context context) { - // TODO: get state directly from service, create a ServiceConnection. - // see: https://github.com/ena1106/FragmentBoundServiceExample/blob/master/app/src/main/java/it/ena1106/fragmentboundservice/BoundFragment.java - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); - mTrackerServiceRunning = settings.getBoolean(PREFS_TRACKER_SERVICE_RUNNING, false); - } - - - /** - * Inner class: SettingsContentObserver is a custom ContentObserver for changes in Android Settings - */ - private class SettingsContentObserver extends ContentObserver { - - SettingsContentObserver(Handler handler) { - super(handler); - } - - @Override - public boolean deliverSelfNotifications() { - return super.deliverSelfNotifications(); - } - - @Override - public void onChange(boolean selfChange) { - super.onChange(selfChange); - LogHelper.v(LOG_TAG, "System Setting change detected."); - - // check if location setting was changed - boolean previousLocationSystemSetting = mLocationSystemSetting; - mLocationSystemSetting = LocationHelper.checkLocationSystemSetting(mActivity); - if (previousLocationSystemSetting != mLocationSystemSetting) { - LogHelper.v(LOG_TAG, "Location Setting change detected."); - toggleLocationOffBar(); - } - - // start / stop preliminary tracking - if (!mLocationSystemSetting) { - stopPreliminaryTracking(); - } else if (!mTrackerServiceRunning && mFragmentVisible) { - startPreliminaryTracking(); - } - } - - } - - - /** - * Inner class: Saves track to external storage using AsyncTask - */ - private class SaveTrackAsyncHelper extends AsyncTask { - - @Override - protected Void doInBackground(Void... voids) { - LogHelper.v(LOG_TAG, "Saving track object in background."); - // save track object - mStorageHelper.saveTrack(mTrack, FILE_MOST_CURRENT_TRACK); - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - super.onPostExecute(aVoid); - // clear track object - LogHelper.v(LOG_TAG, "Saving finished."); - mTrack = null; - - // notify track fragment that save is finished - Intent i = new Intent(); - i.setAction(ACTION_TRACK_SAVE); - i.putExtra(EXTRA_SAVE_FINISHED, true); - LocalBroadcastManager.getInstance(mActivity).sendBroadcast(i); - } - } - /** - * End of inner class - */ - - - /** - * Inner class: Loads track from external storage using AsyncTask - */ - private class LoadTempTrackAsyncHelper extends AsyncTask { - - @Override - protected Void doInBackground(Void... voids) { - LogHelper.v(LOG_TAG, "Loading temporary track object in background."); - // load track object - mTrack = mStorageHelper.loadTrack(FILE_TEMP_TRACK); - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - super.onPostExecute(aVoid); - LogHelper.v(LOG_TAG, "Loading finished."); - - // draw track on map - if (mTrack != null) { - drawTrackOverlay(mTrack); - } - - // delete temp file -// mStorageHelper.deleteTempFile(); // todo check if necessary - } - } - /** - * End of inner class - */ - -} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/MainActivityTrackFragment.java b/app/src/main/java/org/y20k/trackbook/MainActivityTrackFragment.java deleted file mode 100755 index c87ffc3..0000000 --- a/app/src/main/java/org/y20k/trackbook/MainActivityTrackFragment.java +++ /dev/null @@ -1,729 +0,0 @@ -/** - * MainActivityTrackFragment.java - * Implements the track fragment used in the track tab of the main activity - * This fragment displays a saved track - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook; - -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.location.Location; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Vibrator; -import android.view.GestureDetector; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ImageButton; -import android.widget.LinearLayout; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.constraintlayout.widget.Group; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import com.google.android.material.bottomsheet.BottomSheetBehavior; - -import org.osmdroid.api.IMapController; -import org.osmdroid.events.MapEventsReceiver; -import org.osmdroid.tileprovider.tilesource.TileSourceFactory; -import org.osmdroid.util.GeoPoint; -import org.osmdroid.views.MapView; -import org.osmdroid.views.overlay.ItemizedIconOverlay; -import org.osmdroid.views.overlay.MapEventsOverlay; -import org.osmdroid.views.overlay.TilesOverlay; -import org.osmdroid.views.overlay.compass.CompassOverlay; -import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider; -import org.y20k.trackbook.core.Track; -import org.y20k.trackbook.helpers.DialogHelper; -import org.y20k.trackbook.helpers.DropdownAdapter; -import org.y20k.trackbook.helpers.ExportHelper; -import org.y20k.trackbook.helpers.LengthUnitHelper; -import org.y20k.trackbook.helpers.LocationHelper; -import org.y20k.trackbook.helpers.LogHelper; -import org.y20k.trackbook.helpers.MapHelper; -import org.y20k.trackbook.helpers.NightModeHelper; -import org.y20k.trackbook.helpers.StorageHelper; -import org.y20k.trackbook.helpers.TrackbookKeys; - -import java.io.File; -import java.text.DateFormat; -import java.util.Locale; - - -/** - * MainActivityTrackFragment class - */ -public class MainActivityTrackFragment extends Fragment implements AdapterView.OnItemSelectedListener, MapEventsReceiver, TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = MainActivityTrackFragment.class.getSimpleName(); - - - /* Main class variables */ - private FragmentActivity mActivity; - private View mRootView; - private MapView mMapView; - private LinearLayout mOnboardingView; - private IMapController mController; - private ItemizedIconOverlay mTrackOverlay; - private DropdownAdapter mDropdownAdapter; - private ConstraintLayout mTrackManagementLayout; - private Spinner mDropdown; - private View mStatisticsSheet; - private View mStatisticsView; - private TextView mDistanceView; - private TextView mStepsView; - private TextView mWaypointsView; - private TextView mDurationView; - private TextView mRecordingStartView; - private TextView mRecordingStopView; - private TextView mMaxAltitudeView; - private TextView mMinAltitudeView; - private TextView mPositiveElevationView; - private TextView mNegativeElevationView; - private Group mElevationDataViews; - private Group mStatisticsHeaderViews; - private BottomSheetBehavior mStatisticsSheetBehavior; - private int mCurrentTrack; - private Track mTrack; - private BroadcastReceiver mTrackSavedReceiver; - - - /* Return a new Instance of MainActivityTrackFragment */ - public static MainActivityTrackFragment newInstance() { - return new MainActivityTrackFragment(); - } - - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // action bar has options menu - setHasOptionsMenu(true); - - // store activity - mActivity = getActivity(); - - // get current track - if (savedInstanceState != null) { - mCurrentTrack = savedInstanceState.getInt(INSTANCE_CURRENT_TRACK, 0); - } else { - mCurrentTrack = 0; - } - - // create drop-down adapter - mDropdownAdapter = new DropdownAdapter(mActivity); - - // listen for finished save operation - mTrackSavedReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.hasExtra(EXTRA_SAVE_FINISHED) && intent.getBooleanExtra(EXTRA_SAVE_FINISHED, false)) { - LogHelper.v(LOG_TAG, "Save operation detected. Start loading the new track."); - - // update dropdown menu (and load track in onItemSelected) - mDropdownAdapter.refresh(); - mDropdownAdapter.notifyDataSetChanged(); - mDropdown.setAdapter(mDropdownAdapter); - mDropdown.setSelection(0, true); - - // remove onboarding if necessary - switchOnboardingLayout(); - } - } - }; - IntentFilter trackSavedReceiverIntentFilter = new IntentFilter(ACTION_TRACK_SAVE); - LocalBroadcastManager.getInstance(mActivity).registerReceiver(mTrackSavedReceiver, trackSavedReceiverIntentFilter); - - } - - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - - // inflate root view from xml - mRootView = inflater.inflate(R.layout.fragment_main_track, container, false); - - // get reference to onboarding layout - mOnboardingView = (LinearLayout) mRootView.findViewById(R.id.track_tab_onboarding); - - // get reference to basic map - mMapView = (MapView) mRootView.findViewById(R.id.track_map); - - // get map controller - mController = mMapView.getController(); - - // basic map setup - mMapView.setTileSource(TileSourceFactory.MAPNIK); - mMapView.setTilesScaledToDpi(true); - - // set dark map tiles, if necessary - if (NightModeHelper.getNightMode(mActivity)) { - mMapView.getOverlayManager().getTilesOverlay().setColorFilter(TilesOverlay.INVERT_COLORS); - } - - // add multi-touch capability - mMapView.setMultiTouchControls(true); - - // disable default zoom controls - mMapView.getZoomController().setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER); - - // add compass to map - CompassOverlay compassOverlay = new CompassOverlay(mActivity, new InternalCompassOrientationProvider(mActivity), mMapView); - compassOverlay.enableCompass(); - // move the compass overlay down a bit - compassOverlay.setCompassCenter(35.0f, 96.0f); - mMapView.getOverlays().add(compassOverlay); - - // initiate map state - if (savedInstanceState != null) { - // restore saved instance of map - GeoPoint position = new GeoPoint(savedInstanceState.getDouble(INSTANCE_LATITUDE_TRACK_MAP, DEFAULT_LATITUDE), savedInstanceState.getDouble(INSTANCE_LONGITUDE_TRACK_MAP, DEFAULT_LONGITUDE)); - mController.setCenter(position); - mController.setZoom(savedInstanceState.getDouble(INSTANCE_ZOOM_LEVEL_MAIN_MAP, 16f)); - } else { - mController.setZoom(16f); - } - - // get views for track selector - mTrackManagementLayout = (ConstraintLayout) mRootView.findViewById(R.id.track_management_layout); - mDropdown = (Spinner) mRootView.findViewById(R.id.track_selector); - - // attach listeners to export and delete buttons - ImageButton shareButton = (ImageButton) mRootView.findViewById(R.id.share_button); - ImageButton exportButton = (ImageButton) mRootView.findViewById(R.id.export_button); - ImageButton deleteButton = (ImageButton) mRootView.findViewById(R.id.delete_button); - shareButton.setOnClickListener(getShareButtonListener()); - exportButton.setOnClickListener(getExportButtonListener()); - deleteButton.setOnClickListener(getDeleteButtonListener()); - - // get views for statistics sheet - mStatisticsView = mRootView.findViewById(R.id.statistics_view); - mStatisticsSheet = mRootView.findViewById(R.id.statistics_sheet); - mDistanceView = (TextView) mRootView.findViewById(R.id.statistics_data_distance); - mStepsView = (TextView) mRootView.findViewById(R.id.statistics_data_steps); - mWaypointsView = (TextView) mRootView.findViewById(R.id.statistics_data_waypoints); - mDurationView = (TextView) mRootView.findViewById(R.id.statistics_data_duration); - mRecordingStartView = (TextView) mRootView.findViewById(R.id.statistics_data_recording_start); - mRecordingStopView = (TextView) mRootView.findViewById(R.id.statistics_data_recording_stop); - mMaxAltitudeView = (TextView) mRootView.findViewById(R.id.statistics_data_max_altitude); - mMinAltitudeView = (TextView) mRootView.findViewById(R.id.statistics_data_min_altitude); - mPositiveElevationView = (TextView) mRootView.findViewById(R.id.statistics_data_positive_elevation); - mNegativeElevationView = (TextView) mRootView.findViewById(R.id.statistics_data_negative_elevation); - mElevationDataViews = (Group) mRootView.findViewById(R.id.elevation_data); - mStatisticsHeaderViews = (Group) mRootView.findViewById(R.id.statistics_header); - - - // display map and statistics - if (savedInstanceState != null) { - // get track from saved instance and display map and statistics - mTrack = savedInstanceState.getParcelable(INSTANCE_TRACK_TRACK_MAP); - displayTrack(); - } else if (mTrack == null) { - // load track and display map and statistics - LoadTrackAsyncHelper loadTrackAsyncHelper = new LoadTrackAsyncHelper(); - loadTrackAsyncHelper.execute(); - } else { - // just display map and statistics - displayTrack(); - } - - // set up and show statistics sheet - mStatisticsSheetBehavior = BottomSheetBehavior.from(mStatisticsSheet); - mStatisticsSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - mStatisticsSheetBehavior.setBottomSheetCallback(getStatisticsSheetCallback()); - - // attach listener for taps on elevation views - attachTapListenerToElevationViews(); - - // attach listener for taps on statistics sheet header - attachTapListenerToStatisticHeaderViews(); - - // attach listener for taps on statistics - for US and other states plagued by Imperial units - if (LengthUnitHelper.getUnitSystem() == IMPERIAL || Locale.getDefault().getCountry().equals("GB")) { - attachTapListenerToStatisticsSheet(); - } - - // enable additional gestures - MapEventsOverlay OverlayEventos = new MapEventsOverlay(this); - mMapView.getOverlays().add(OverlayEventos); - - return mRootView; - } - - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - mDropdown.setAdapter(mDropdownAdapter); - mDropdown.setOnItemSelectedListener(this); - } - - - @Override - public void onResume() { - super.onResume(); - // show / hide the onboarding layout - switchOnboardingLayout(); - } - - - @Override - public void onPause() { - super.onPause(); - } - - - @Override - public void onDestroyView(){ - super.onDestroyView(); - - // deactivate map - mMapView.onDetach(); - } - - - @Override - public void onDestroy() { - LogHelper.v(LOG_TAG, "onDestroy called."); - - // remove listener - LocalBroadcastManager.getInstance(mActivity).unregisterReceiver(mTrackSavedReceiver); - - super.onDestroy(); - } - - - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - // update current track - mCurrentTrack = i; - - // load track and display map and statistics - LoadTrackAsyncHelper loadTrackAsyncHelper = new LoadTrackAsyncHelper(); - loadTrackAsyncHelper.execute(i); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - - } - - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putDouble(INSTANCE_LATITUDE_TRACK_MAP, mMapView.getMapCenter().getLatitude()); - outState.putDouble(INSTANCE_LONGITUDE_TRACK_MAP, mMapView.getMapCenter().getLongitude()); - outState.putDouble(INSTANCE_ZOOM_LEVEL_TRACK_MAP, mMapView.getZoomLevelDouble()); - outState.putParcelable(INSTANCE_TRACK_TRACK_MAP, mTrack); - outState.putInt(INSTANCE_CURRENT_TRACK, mCurrentTrack); - super.onSaveInstanceState(outState); - } - - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - switch(requestCode) { - case RESULT_DELETE_DIALOG: - if (resultCode == Activity.RESULT_OK) { - deleteCurrentTrack(); - } else if (resultCode == Activity.RESULT_CANCELED){ - LogHelper.v(LOG_TAG, "Delete dialog result: CANCEL"); - } - break; - case RESULT_EXPORT_DIALOG: - if (resultCode == Activity.RESULT_OK) { - // user chose EXPORT - ExportHelper.exportToGpx(mActivity, mTrack); - } else if (resultCode == Activity.RESULT_CANCELED){ - // User chose CANCEL - LogHelper.v(LOG_TAG, "Export to GPX: User chose CANCEL."); - } - break; - } - } - - - @Override - public boolean singleTapConfirmedHelper(GeoPoint p) { - return false; - } - - - @Override - public boolean longPressHelper(GeoPoint p) { - if (mTrack != null) { - // vibrate 50 milliseconds - Vibrator vibrator = (Vibrator) mActivity.getSystemService(Context.VIBRATOR_SERVICE); - vibrator.vibrate(50); - // zoom to bounding box (= edge coordinates of map) - mMapView.zoomToBoundingBox(mTrack.getBoundingBox(), true); - } - return true; - } - - - /* Displays map and statistics for track */ - private void displayTrack() { - GeoPoint position; - - if (mTrack != null && mTrack.getSize() > 0) { - // set end of track as position - Location lastLocation = mTrack.getWayPointLocation(mTrack.getSize() -1); - position = new GeoPoint(lastLocation.getLatitude(), lastLocation.getLongitude()); - - String recordingStart = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()).format(mTrack.getRecordingStart()) + " " + - DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()).format(mTrack.getRecordingStart()); - String recordingStop = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()).format(mTrack.getRecordingStop()) + " " + - DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()).format(mTrack.getRecordingStop()); - String stepsTaken; - if (mTrack.getStepCount() == -1) { - stepsTaken = getString(R.string.statistics_sheet_p_steps_no_pedometer); - } else { - stepsTaken = String.valueOf(Math.round(mTrack.getStepCount())); - } - - // populate length views - displayCurrentLengthUnits(); - // populate other views - mStepsView.setText(stepsTaken); - mWaypointsView.setText(String.valueOf(mTrack.getWayPoints().size())); - mDurationView.setText(LocationHelper.convertToReadableTime(mTrack.getTrackDuration(), true)); - mRecordingStartView.setText(recordingStart); - mRecordingStopView.setText(recordingStop); - - // show/hide elevation views depending on file format version - if (mTrack.getTrackFormatVersion() > 1 && mTrack.getMinAltitude() > 0) { - // show elevation views - mElevationDataViews.setVisibility(View.VISIBLE); - } else { - // hide elevation views - mElevationDataViews.setVisibility(View.GONE); - } - - // draw track on map - drawTrackOverlay(mTrack); - } else { - position = new GeoPoint(DEFAULT_LATITUDE, DEFAULT_LONGITUDE); - } - - // center map over position - mController.setCenter(position); - - } - - - /* Draws track onto overlay */ - private void drawTrackOverlay(Track track) { - mMapView.getOverlays().remove(mTrackOverlay); - mTrackOverlay = MapHelper.createTrackOverlay(mActivity, track, false); - mMapView.getOverlays().add(mTrackOverlay); - } - - - /* show the onboarding layout, if no track has been recorded yet */ - private void switchOnboardingLayout() { - if (mDropdownAdapter.isEmpty()){ - // show onboarding layout - mMapView.setVisibility(View.GONE); - mOnboardingView.setVisibility(View.VISIBLE); - mTrackManagementLayout.setVisibility(View.GONE); - mStatisticsSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - mStatisticsSheet.setVisibility(View.GONE); - } else { - // show normal layout - mOnboardingView.setVisibility(View.GONE); - mMapView.setVisibility(View.VISIBLE); - mTrackManagementLayout.setVisibility(View.VISIBLE); - mStatisticsSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - mStatisticsSheet.setVisibility(View.VISIBLE); - } - } - - - /* Displays views in statistic sheet according to current locale */ - private void displayCurrentLengthUnits() { - mDistanceView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getTrackDistance())); - mPositiveElevationView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getPositiveElevation())); - mNegativeElevationView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getNegativeElevation())); - mMaxAltitudeView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getMaxAltitude())); - mMinAltitudeView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getMinAltitude())); - } - - - /* Switches views in statistic sheet between Metric and Imperial */ - private void displayOppositeLengthUnits() { - int oppositeLengthUnit = LengthUnitHelper.getUnitSystem() * -1; - mDistanceView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getTrackDistance(), oppositeLengthUnit)); - mPositiveElevationView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getPositiveElevation(), oppositeLengthUnit)); - mNegativeElevationView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getNegativeElevation(), oppositeLengthUnit)); - mMaxAltitudeView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getMaxAltitude(), oppositeLengthUnit)); - mMinAltitudeView.setText(LengthUnitHelper.convertDistanceToString(mTrack.getMinAltitude(), oppositeLengthUnit)); - } - - - /* Deletes currently visible track */ - private void deleteCurrentTrack() { - - // delete track file and refresh dropdown adapter - if (mDropdownAdapter.getItem(mCurrentTrack).getTrackFile().delete()) { - mDropdownAdapter.refresh(); - mDropdownAdapter.notifyDataSetChanged(); - mDropdown.setAdapter(mDropdownAdapter); - } else { - LogHelper.e(LOG_TAG, "Unable to delete recording."); - return; - } - - if (mDropdownAdapter.isEmpty()) { - // show onboarding - switchOnboardingLayout(); - } else { - // show next track - mDropdown.setSelection(0, true); - mCurrentTrack = 0; - } - - } - - - /* Creates BottomSheetCallback for the statistics sheet - needed in onCreateView */ - private BottomSheetBehavior.BottomSheetCallback getStatisticsSheetCallback() { - return new BottomSheetBehavior.BottomSheetCallback() { - @Override - public void onStateChanged(@NonNull View bottomSheet, int newState) { - // react to state change - switch (newState) { - case BottomSheetBehavior.STATE_EXPANDED: - // statistics sheet expanded - mTrackManagementLayout.setVisibility(View.INVISIBLE); - mStatisticsSheet.setBackgroundColor(ContextCompat.getColor(mActivity, R.color.statistic_sheet_background_expanded)); - break; - case BottomSheetBehavior.STATE_COLLAPSED: - // statistics sheet collapsed - mTrackManagementLayout.setVisibility(View.VISIBLE); - mStatisticsSheet.setBackgroundColor(ContextCompat.getColor(mActivity, R.color.statistic_sheet_background_collapsed)); - mStatisticsSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - break; - case BottomSheetBehavior.STATE_HIDDEN: - // statistics sheet hidden - mStatisticsSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - break; - default: - break; - } - } - @Override - public void onSlide(@NonNull View bottomSheet, float slideOffset) { - // reset length unit displays - displayCurrentLengthUnits(); - // react to dragging events - if (slideOffset < 0.5f) { - mTrackManagementLayout.setVisibility(View.VISIBLE); - } else { - mTrackManagementLayout.setVisibility(View.INVISIBLE); - } - if (slideOffset < 0.125f) { - mStatisticsSheet.setBackgroundColor(ContextCompat.getColor(mActivity, R.color.statistic_sheet_background_collapsed)); - } else { - mStatisticsSheet.setBackgroundColor(ContextCompat.getColor(mActivity, R.color.statistic_sheet_background_expanded)); - } - } - }; - } - - - /* Creates OnClickListener for the export button - needed in onCreateView */ - private View.OnClickListener getShareButtonListener() { - return new View.OnClickListener() { - @Override - public void onClick(View view) { - Intent intent = ExportHelper.getGpxFileIntent(mActivity, mTrack); - // create intent to show chooser - String title = getString(R.string.dialog_share_gpx); -// String title = getResources().getString(R.string.chooser_title); - Intent chooser = Intent.createChooser(intent, title); - if (intent.resolveActivity(mActivity.getPackageManager()) != null) { - startActivity(chooser); - } else { - Toast.makeText(mActivity, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show(); - } - } - }; - } - - - /* Creates OnClickListener for the export button - needed in onCreateView */ - private View.OnClickListener getExportButtonListener() { - return new View.OnClickListener() { - @Override - public void onClick(View view) { - // dialog text components - int dialogTitle; - int dialogPositiveButton; - int dialogNegativeButton; - DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()); - String recordingStartDate = df.format(mTrack.getRecordingStart()); - String dialogMessage; - - // get text elements for delete dialog - if (ExportHelper.gpxFileExists(mTrack)) { - // CASE: OVERWRITE - GPX file exists - dialogTitle = R.string.dialog_export_title_overwrite; - dialogMessage = getString(R.string.dialog_export_content_overwrite) + " (" + recordingStartDate + " | " + LengthUnitHelper.convertDistanceToString(mTrack.getTrackDistance()) + ")"; - dialogPositiveButton = R.string.dialog_export_action_overwrite; - dialogNegativeButton = R.string.dialog_default_action_cancel; - } else { - // CASE: EXPORT - GPX file does NOT yet exits - dialogTitle = R.string.dialog_export_title_export; - dialogMessage = getString(R.string.dialog_export_content_export) + " (" + recordingStartDate + " | " + LengthUnitHelper.convertDistanceToString(mTrack.getTrackDistance()) + ")"; - dialogPositiveButton = R.string.dialog_export_action_export; - dialogNegativeButton = R.string.dialog_default_action_cancel; - } - - // show delete dialog - results are handles by onActivityResult - DialogFragment dialogFragment = DialogHelper.newInstance(dialogTitle, dialogMessage, dialogPositiveButton, dialogNegativeButton); - dialogFragment.setTargetFragment(MainActivityTrackFragment.this, RESULT_EXPORT_DIALOG); - dialogFragment.show(mActivity.getSupportFragmentManager(), "ExportDialog"); - } - }; - } - - - /* Creates OnClickListener for the delete button - needed in onCreateView */ - private View.OnClickListener getDeleteButtonListener() { - return new View.OnClickListener() { - @Override - public void onClick(View view) { - // get text elements for delete dialog - int dialogTitle = R.string.dialog_delete_title; - int dialogPositiveButton = R.string.dialog_delete_action_delete; - int dialogNegativeButton = R.string.dialog_default_action_cancel; - DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()); - String recordingStartDate = df.format(mTrack.getRecordingStart()); - String dialogMessage = getString(R.string.dialog_delete_content) + " " + recordingStartDate + " | " + LengthUnitHelper.convertDistanceToString(mTrack.getTrackDistance()); - - // show delete dialog - results are handles by onActivityResult - DialogFragment dialogFragment = DialogHelper.newInstance(dialogTitle, dialogMessage, dialogPositiveButton, dialogNegativeButton); - dialogFragment.setTargetFragment(MainActivityTrackFragment.this, RESULT_DELETE_DIALOG); - dialogFragment.show(mActivity.getSupportFragmentManager(), "DeleteDialog"); - } - }; - } - - - /* Add tap listener to elevation data views */ - private void attachTapListenerToElevationViews() { - int referencedIds[] = mElevationDataViews.getReferencedIds(); - for (int id : referencedIds) { - mRootView.findViewById(id).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - // inform user about possible issues with altitude measurements - Toast.makeText(mActivity, R.string.toast_message_elevation_info, Toast.LENGTH_LONG).show(); - } - }); - } - } - - - /* Add tap listener to statistic header views */ - private void attachTapListenerToStatisticHeaderViews() { - int referencedIds[] = mStatisticsHeaderViews.getReferencedIds(); - for (int id : referencedIds) { - mRootView.findViewById(id).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (mStatisticsSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { - mStatisticsSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); - } else { - mStatisticsSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); - } - } - }); - } - } - - - /* Add tap listener to statistics sheet */ - private void attachTapListenerToStatisticsSheet() { - mStatisticsView.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - if(event.getAction() == MotionEvent.ACTION_DOWN) { - displayOppositeLengthUnits(); - } else if (event.getAction() == MotionEvent.ACTION_UP) { - displayCurrentLengthUnits(); - } - return true; - } - }); - } - - - /** - * Inner class: Loads track from external storage using AsyncTask - */ - private class LoadTrackAsyncHelper extends AsyncTask { - - @Override - protected Void doInBackground(Integer... ints) { - LogHelper.v(LOG_TAG, "Loading track object in background."); - - StorageHelper storageHelper = new StorageHelper(mActivity); - if (ints.length > 0) { - // get track file from dropdown adapter - int item = ints[0]; - File trackFile = mDropdownAdapter.getItem(item).getTrackFile(); - LogHelper.v(LOG_TAG, "Loading track number " + item); - mTrack = storageHelper.loadTrack(trackFile); - } else { - // load track object from most current file - LogHelper.v(LOG_TAG, "No specific track specified. Loading most current one."); - mTrack = storageHelper.loadTrack(FILE_MOST_CURRENT_TRACK); - } - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - super.onPostExecute(aVoid); - - // display track on map - displayTrack(); - } - } - /** - * End of inner class - */ - -} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/MapFragment.kt b/app/src/main/java/org/y20k/trackbook/MapFragment.kt new file mode 100644 index 0000000..da1747e --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/MapFragment.kt @@ -0,0 +1,289 @@ +/* + * MapFragment.kt + * Implements the MapFragment fragment + * A MapFragment displays a map using osmdroid as well as the controls to start / stop a recording + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook + +import YesNoDialog +import android.Manifest +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.content.pm.PackageManager +import android.location.Location +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.IBinder +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.y20k.trackbook.core.Track +import org.y20k.trackbook.core.TracklistElement +import org.y20k.trackbook.helpers.* +import org.y20k.trackbook.ui.MapFragmentLayoutHolder + + +/* + * MapFragment class + */ +class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(MapFragment::class.java) + + + /* Main class variables */ + private var bound: Boolean = false + private val handler: Handler = Handler() + private var trackingState: Int = Keys.STATE_NOT_TRACKING + private var gpsProviderActive: Boolean = false + private var networkProviderActive: Boolean = false + private var track: Track = Track() + private lateinit var currentBestLocation: Location + private lateinit var layout: MapFragmentLayoutHolder + private lateinit var trackerService: TrackerService + + + /* Overrides onCreate from Fragment */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // get current best location + currentBestLocation = LocationHelper.getLastKnownLocation(activity as Context) + // get saved tracking state + trackingState = PreferencesHelper.loadTrackingState(activity as Context) + } + + + /* Overrides onStop from Fragment */ + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + // initialize layout + layout = MapFragmentLayoutHolder(activity as Context, inflater, container, currentBestLocation, trackingState) + + // set up buttons + layout.currentLocationButton.setOnClickListener { + layout.centerMap(currentBestLocation, animated = true) + } + layout.recordingButton.setOnClickListener { + handleTrackingManagementMenu() + } + layout.saveButton.setOnClickListener { + saveTrack() + } + layout.clearButton.setOnClickListener { + trackerService.clearTrack() + } + layout.resumeButton.setOnClickListener { + // start service via intent so that it keeps running after unbind + startTrackerService() + trackerService.resumeTracking() + } + + return layout.rootView + } + + + /* Overrides onStart from Fragment */ + override fun onStart() { + super.onStart() + // request location permission if denied + if (ContextCompat.checkSelfPermission(activity as Context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) { + this.requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), Keys.REQUEST_CODE_FOREGROUND) + } + // bind to TrackerService + activity?.bindService(Intent(activity, TrackerService::class.java), connection, Context.BIND_AUTO_CREATE) + } + + + /* Overrides onResume from Fragment */ + override fun onResume() { + super.onResume() + // show hide the location error snackbar + layout.toggleLocationErrorBar(gpsProviderActive, networkProviderActive) + // set map center + layout.centerMap(currentBestLocation) + } + + + /* Overrides onPause from Fragment */ + override fun onPause() { + super.onPause() + layout.saveState(currentBestLocation) + } + + + /* Overrides onStop from Fragment */ + override fun onStop() { + super.onStop() + // unbind from TrackerService + activity?.unbindService(connection) + } + + + /* Overrides onRequestPermissionsResult from Fragment */ + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + when (requestCode) { + Keys.REQUEST_CODE_FOREGROUND -> { + if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { + // permission was granted - re-bind service + activity?.unbindService(connection) + activity?.bindService(Intent(activity, TrackerService::class.java), connection, Context.BIND_AUTO_CREATE) + LogHelper.i(TAG, "Request result: Location permission has been granted.") + } else { + // permission denied - unbind service + activity?.unbindService(connection) + } + layout.toggleLocationErrorBar(gpsProviderActive, networkProviderActive) + return + } + } + } + + + /* Overrides onYesNoDialog from YesNoDialogListener */ + override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) { + super.onYesNoDialog(type, dialogResult, payload, payloadString) + when (type) { + Keys.DIALOG_EMPTY_RECORDING -> { + when (dialogResult) { + // user tapped resume + true -> { + trackerService.resumeTracking() + } + } + } + } + } + + + /* Start tracker service */ + private fun startTrackerService() { + val intent = Intent(activity, TrackerService::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // ... start service in foreground to prevent it being killed on Oreo + activity?.startForegroundService(intent) + } else { + activity?.startService(intent) + } + } + + + /* Starts / pauses tracking and toggles the recording sub menu_bottom_navigation */ + private fun handleTrackingManagementMenu() { + when (trackingState) { + Keys.STATE_TRACKING_STOPPED -> layout.toggleRecordingButtonSubMenu() + Keys.STATE_TRACKING_ACTIVE -> trackerService.stopTracking() + Keys.STATE_NOT_TRACKING -> { + // start service via intent so that it keeps running after unbind + startTrackerService() + trackerService.startTracking() + } + } + } + + + /* Saves track - shows dialog, if recording is still empty */ + private fun saveTrack() { + if (track.wayPoints.isEmpty()) { + YesNoDialog(this as YesNoDialog.YesNoDialogListener).show(activity as Context, type = Keys.DIALOG_EMPTY_RECORDING, title = R.string.dialog_error_empty_recording_title, message = R.string.dialog_error_empty_recording_message, yesButton = R.string.dialog_error_empty_recording_action_resume) + } else { + GlobalScope.launch { + // step 1: create and store filenames for json and gpx files + track.trackUriString = FileHelper.getTrackFileUri(activity as Context, track).toString() + track.gpxUriString = FileHelper.getGpxFileUri(activity as Context, track).toString() + // step 2: save track + FileHelper.saveTrackSuspended(track, saveGpxToo = true) + // step 3: save tracklist - suspended + FileHelper.addTrackAndSaveTracklistSuspended(activity as Context, track) + // step 3: clear track + trackerService.clearTrack() + // step 4: open track in TrackFragement + openTrack(track.toTracklistElement(activity as Context)) + } + } + } + + + /* Opens a track in TrackFragment */ + private fun openTrack(tracklistElement: TracklistElement) { + val bundle: Bundle = Bundle() + bundle.putString(Keys.ARG_TRACK_TITLE, tracklistElement.name) + bundle.putString(Keys.ARG_TRACK_FILE_URI, tracklistElement.trackUriString) + bundle.putString(Keys.ARG_GPX_FILE_URI, tracklistElement.gpxUriString) + bundle.putLong(Keys.ARG_TRACK_ID, TrackHelper.getTrackId(tracklistElement)) + findNavController().navigate(R.id.fragment_track, bundle) + } + + + /* + * Defines callbacks for service binding, passed to bindService() + */ + private val connection = object : ServiceConnection { + override fun onServiceConnected(className: ComponentName, service: IBinder) { + // We've bound to LocalService, cast the IBinder and get LocalService instance + val binder = service as TrackerService.LocalBinder + trackerService = binder.service + bound = true + // start listening for location updates + handler.removeCallbacks(periodicLocationRequestRunnable) + handler.postDelayed(periodicLocationRequestRunnable, 0) + } + override fun onServiceDisconnected(arg0: ComponentName) { + bound = false + // stop receiving location updates + handler.removeCallbacks(periodicLocationRequestRunnable) + } + } + /* + * End of declaration + */ + + + /* + * Runnable: Periodically requests location + */ + private val periodicLocationRequestRunnable: Runnable = object : Runnable { + override fun run() { + // pull values from service + currentBestLocation = trackerService.currentBestLocation + track = trackerService.track + gpsProviderActive = trackerService.gpsProviderActive + networkProviderActive = trackerService.networkProviderActive + trackingState = trackerService.trackingState + // update location and track + layout.markCurrentPosition(currentBestLocation, trackingState) + layout.overlayCurrentTrack(track, trackingState) + layout.updateRecordingButton(trackingState) + // center map, if it had not been dragged/zoomed before + if (!layout.userInteraction) { layout.centerMap(currentBestLocation, true)} + // show error snackbar if necessary + layout.toggleLocationErrorBar(gpsProviderActive, networkProviderActive) + // use the handler to start runnable again after specified delay + handler.postDelayed(this, Keys.REQUEST_CURRENT_LOCATION_INTERVAL) + } + } + /* + * End of declaration + */ + +} diff --git a/app/src/main/java/org/y20k/trackbook/SettingsFragment.kt b/app/src/main/java/org/y20k/trackbook/SettingsFragment.kt new file mode 100644 index 0000000..d09417f --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/SettingsFragment.kt @@ -0,0 +1,109 @@ +/* + * SettingsFragment.kt + * Implements the SettingsFragment fragment + * A SettingsFragment displays the user accessible settings of the app + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook + + +import android.content.Context +import android.os.Bundle +import android.view.View +import androidx.preference.* +import org.y20k.trackbook.helpers.LengthUnitHelper +import org.y20k.trackbook.helpers.LogHelper + + +/* + * SettingsFragment class + */ +class SettingsFragment : PreferenceFragmentCompat() { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(SettingsFragment::class.java) + + + /* Overrides onViewCreated from PreferenceFragmentCompat */ + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + // set the background color + view.setBackgroundColor(resources.getColor(R.color.app_window_background, null)) + // add padding - necessary because translucent status bar is used + val topPadding = this.resources.displayMetrics.density * 24 // 24 dp * display density + view.setPadding(0, topPadding.toInt(), 0, 0) + } + + + /* Overrides onCreatePreferences from PreferenceFragmentCompat */ + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + + val context = preferenceManager.context + val screen = preferenceManager.createPreferenceScreen(context) + + // set up "Enable Imperial Measurements" preference + val preferenceImperialMeasurementUnits: SwitchPreferenceCompat = SwitchPreferenceCompat(activity as Context) + preferenceImperialMeasurementUnits.title = getString(R.string.pref_imperial_measurement_units_title) + preferenceImperialMeasurementUnits.key = Keys.PREF_USE_IMPERIAL_UNITS + preferenceImperialMeasurementUnits.summaryOn = getString(R.string.pref_imperial_measurement_units_summary_imperial) + preferenceImperialMeasurementUnits.summaryOff = getString(R.string.pref_imperial_measurement_units_summary_metric) + preferenceImperialMeasurementUnits.setDefaultValue(LengthUnitHelper.useImperialUnits()) + + // set up "Restrict to GPS" preference + val preferenceGpsOnly: SwitchPreferenceCompat = SwitchPreferenceCompat(activity as Context) + preferenceGpsOnly.title = getString(R.string.pref_gps_only_title) + preferenceGpsOnly.key = Keys.PREF_GPS_ONLY + preferenceGpsOnly.summaryOn = getString(R.string.pref_gps_only_summary_gps_only) + preferenceGpsOnly.summaryOff = getString(R.string.pref_gps_only_summary_gps_and_network) + preferenceGpsOnly.setDefaultValue(false) + + // set up "Accuracy Threshold" preference + val preferenceAccuracyThreshold: SeekBarPreference = SeekBarPreference(activity as Context) + preferenceAccuracyThreshold.title = getString(R.string.pref_accuracy_threshold_title) + preferenceAccuracyThreshold.key = Keys.PREF_LOCATION_ACCURACY_THRESHOLD + preferenceAccuracyThreshold.summary = getString(R.string.pref_accuracy_threshold_summary) + preferenceAccuracyThreshold.showSeekBarValue = true + preferenceAccuracyThreshold.max = 50 + preferenceAccuracyThreshold.setDefaultValue(Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY) + + // set up "Reset" preference + val preferenceResetAdvanced: Preference = Preference(activity as Context) + preferenceResetAdvanced.title = getString(R.string.pref_reset_advanced_title) + preferenceResetAdvanced.summary = getString(R.string.pref_reset_advanced_summary) + preferenceResetAdvanced.setOnPreferenceClickListener{ + preferenceAccuracyThreshold.value = Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY + return@setOnPreferenceClickListener true + } + + // set preference categories + val preferenceCategoryGeneral: PreferenceCategory = PreferenceCategory(activity as Context) + preferenceCategoryGeneral.title = getString(R.string.pref_general_title) + preferenceCategoryGeneral.contains(preferenceImperialMeasurementUnits) + preferenceCategoryGeneral.contains(preferenceGpsOnly) + val preferenceCategoryAdvanced: PreferenceCategory = PreferenceCategory(activity as Context) + preferenceCategoryAdvanced.title = getString(R.string.pref_advanced_title) + preferenceCategoryAdvanced.contains(preferenceAccuracyThreshold) + preferenceCategoryAdvanced.contains(preferenceResetAdvanced) + + // setup preference screen + screen.addPreference(preferenceCategoryGeneral) + screen.addPreference(preferenceImperialMeasurementUnits) + screen.addPreference(preferenceGpsOnly) + screen.addPreference(preferenceCategoryAdvanced) + screen.addPreference(preferenceAccuracyThreshold) + screen.addPreference(preferenceResetAdvanced) + preferenceScreen = screen + } + +} diff --git a/app/src/main/java/org/y20k/trackbook/TrackFragment.kt b/app/src/main/java/org/y20k/trackbook/TrackFragment.kt new file mode 100644 index 0000000..706fbe1 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/TrackFragment.kt @@ -0,0 +1,143 @@ +/* + * TrackFragment.kt + * Implements the TrackFragment fragment + * A TrackFragment displays a previously recorded track + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook + + +import YesNoDialog +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.content.FileProvider +import androidx.core.net.toFile +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.y20k.trackbook.Keys.ARG_TRACK_ID +import org.y20k.trackbook.dialogs.RenameTrackDialog +import org.y20k.trackbook.helpers.FileHelper +import org.y20k.trackbook.helpers.LogHelper +import org.y20k.trackbook.ui.TrackFragmentLayoutHolder + +class TrackFragment : Fragment(), RenameTrackDialog.RenameTrackListener, YesNoDialog.YesNoDialogListener { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(TrackFragment::class.java) + + + /* Main class variables */ + private lateinit var layout:TrackFragmentLayoutHolder + + + /* Overrides onCreateView from Fragment */ + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + // initialize layout + layout = TrackFragmentLayoutHolder(activity as Context, inflater, container, arguments) + + // set up share button + layout.shareButton.setOnClickListener { + shareGpXTrack() + } + // set up delete button + layout.deleteButton.setOnClickListener { + val dialogMessage: String = "${getString(R.string.dialog_yes_no_message_remove_recording)}\n\n- ${layout.trackNameView.text}" + YesNoDialog(this@TrackFragment as YesNoDialog.YesNoDialogListener).show(context = activity as Context, type = Keys.DIALOG_REMOVE_TRACK, messageString = dialogMessage, yesButton = R.string.dialog_yes_no_positive_button_remove_recording) + } + // set up rename button + layout.editButton.setOnClickListener { + RenameTrackDialog(this as RenameTrackDialog.RenameTrackListener).show(activity as Context, layout.trackNameView.text.toString()) + } + + return layout.rootView + } + + + /* Overrides onResume from Fragment */ + override fun onResume() { + super.onResume() + // update zoom level and map center + layout.updateMapView() + } + + + /* Overrides onPause from Fragment */ + override fun onPause() { + super.onPause() + // save zoom level and map center + layout.saveViewStateToTrack() + } + + + /* Overrides onRenameTrackDialog from RenameTrackDialog */ + override fun onRenameTrackDialog(textInput: String) { + // rename track async (= fire & forget - no return value needed) + GlobalScope.launch { FileHelper.renameTrackSuspended(activity as Context, layout.track, textInput) } + // update name in layout + layout.track.name = textInput + layout.trackNameView.text = textInput + } + + + /* Overrides onYesNoDialog from YesNoDialogListener */ + override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) { + when (type) { + Keys.DIALOG_REMOVE_TRACK -> { + when (dialogResult) { + // user tapped remove track + true -> { + // switch to TracklistFragment and remove track there + val trackId: Long = arguments?.getLong(ARG_TRACK_ID, -1L) ?: -1L + val bundle: Bundle = bundleOf(Keys.ARG_TRACK_ID to trackId) + findNavController().navigate(R.id.tracklist_fragment, bundle) + } + } + } + } + } + + + /* Share track as GPX via share sheet */ + private fun shareGpXTrack() { + val gpxFile = Uri.parse(layout.track.gpxUriString).toFile() + val gpxShareUri = FileProvider.getUriForFile(this.activity as Context, "${activity!!.applicationContext.packageName}.provider", gpxFile) + val shareIntent: Intent = Intent.createChooser(Intent().apply { + action = Intent.ACTION_SEND + data = gpxShareUri + type = "application/gpx+xml" + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + putExtra(Intent.EXTRA_STREAM, gpxShareUri) + putExtra(Intent.EXTRA_TITLE, getString(R.string.dialog_share_gpx)) + }, null) + + // show share sheet - if file helper is available + val packageManager: PackageManager? = activity?.packageManager + if (packageManager != null && shareIntent.resolveActivity(packageManager) != null) { + startActivity(shareIntent) + } else { + Toast.makeText(activity, R.string.toast_message_install_file_helper, Toast.LENGTH_LONG).show() + } + } + +} diff --git a/app/src/main/java/org/y20k/trackbook/Trackbook.java b/app/src/main/java/org/y20k/trackbook/Trackbook.java deleted file mode 100755 index 3be8999..0000000 --- a/app/src/main/java/org/y20k/trackbook/Trackbook.java +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Trackbook.java - * Implements the Trackbook class - * Trackbook starts up the app and sets up the basic theme (Day / Night) - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook; - -import android.app.Application; - -import org.y20k.trackbook.helpers.LogHelper; -import org.y20k.trackbook.helpers.NightModeHelper; - - -/** - * Trackbook.class - */ -public class Trackbook extends Application { - - /* Define log tag */ - private static final String LOG_TAG = Trackbook.class.getSimpleName(); - - - @Override - public void onCreate() { - super.onCreate(); - - // set Day / Night theme state - NightModeHelper.restoreSavedState(this); - -// todo remove -// if (Build.VERSION.SDK_INT >= 28) { -// // Android P might introduce a system wide theme option - in that case: follow system (28 = Build.VERSION_CODES.P) -// AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); -// } else { -// // try to get last state the user chose -// NightModeHelper.restoreSavedState(this); -// } - - } - - - @Override - public void onTerminate() { - super.onTerminate(); - LogHelper.v(LOG_TAG, "Trackbook application terminated."); - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/Trackbook2.kt b/app/src/main/java/org/y20k/trackbook/Trackbook2.kt new file mode 100644 index 0000000..ff04f7a --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/Trackbook2.kt @@ -0,0 +1,51 @@ +/* + * Trackbook.kt + * Implements the Trackbook class + * Trackbook is the base Application class that sets up day and night theme + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + + +package org.y20k.trackbook + +import android.app.Application +import org.y20k.trackbook.helpers.LogHelper +import org.y20k.trackbook.helpers.NightModeHelper + + +/* + * Trackbook.class + */ +class Trackbook: Application() { + + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(Trackbook::class.java) + + + /* Implements onCreate */ + override fun onCreate() { + super.onCreate() + LogHelper.v(TAG, "Trackbook application started.") + // set Day / Night theme state + NightModeHelper.restoreSavedState(this) + } + + + /* Implements onTerminate */ + override fun onTerminate() { + super.onTerminate() + LogHelper.v(TAG, "Trackbook application terminated.") + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/TrackerService.java b/app/src/main/java/org/y20k/trackbook/TrackerService.java deleted file mode 100755 index 121685a..0000000 --- a/app/src/main/java/org/y20k/trackbook/TrackerService.java +++ /dev/null @@ -1,618 +0,0 @@ -/** - * TrackerService.java - * Implements the app's movement tracker service - * The TrackerService creates a Track object and displays a notification - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.ContentObserver; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.os.AsyncTask; -import android.os.Binder; -import android.os.Bundle; -import android.os.CountDownTimer; -import android.os.Handler; -import android.os.IBinder; -import android.preference.PreferenceManager; -import android.widget.Toast; - -import androidx.core.app.NotificationCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import org.y20k.trackbook.core.Track; -import org.y20k.trackbook.helpers.LocationHelper; -import org.y20k.trackbook.helpers.LogHelper; -import org.y20k.trackbook.helpers.NotificationHelper; -import org.y20k.trackbook.helpers.StorageHelper; -import org.y20k.trackbook.helpers.TrackbookKeys; - -import java.util.List; - -import static android.hardware.Sensor.TYPE_STEP_COUNTER; - - -/** - * TrackerService class - */ -public class TrackerService extends Service implements TrackbookKeys, SensorEventListener { - - /* Define log tag */ - private static final String LOG_TAG = TrackerService.class.getSimpleName(); - - - /* Main class variables */ - private Track mTrack; - private CountDownTimer mTimer; - private LocationManager mLocationManager; - private SensorManager mSensorManager; - private float mStepCountOffset; - private LocationListener mGPSListener = null; - private LocationListener mNetworkListener = null; - private SettingsContentObserver mSettingsContentObserver; - private Location mCurrentBestLocation; - private Notification mNotification; - private NotificationCompat.Builder mNotificationBuilder; - private NotificationManager mNotificationManager; - private boolean mTrackerServiceRunning; - private boolean mLocationSystemSetting; - private boolean mResumedFlag; - - private final IBinder mBinder = new LocalBinder(); // todo move to onCreate - - - @Override - public void onCreate() { - super.onCreate(); - - // prepare notification channel and get NotificationManager - mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - NotificationHelper.createNotificationChannel(this); - - // acquire reference to Location Manager - mLocationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE); - - // acquire reference to Sensor Manager - mSensorManager = (SensorManager) this.getSystemService(Context.SENSOR_SERVICE); - - // get state of location system setting - mLocationSystemSetting = LocationHelper.checkLocationSystemSetting(getApplicationContext()); - - // create content observer for changes in System Settings - mSettingsContentObserver = new SettingsContentObserver(new Handler()); - - // initialize the resume flag - mResumedFlag = false; - } - - - @Override - public IBinder onBind(Intent intent) { - // a client is binding to the service with bindService() - return mBinder; - } - - - @Override - public boolean onUnbind(Intent intent) { - // return true if you would like to have the service's onRebind(Intent) method later called when new clients bind to it. - return true; - } - - - @Override - public void onRebind(Intent intent) { - // a client is binding to the service with bindService(), after onUnbind() has already been called - } - - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - - // SERVICE RESTART (via START_STICKY) - if (intent == null) { - if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(PREFS_TRACKER_SERVICE_RUNNING, false)) { - LogHelper.w(LOG_TAG, "Trackbook has been killed by the operating system. Trying to resume recording."); - resumeTracking(LocationHelper.determineLastKnownLocation(mLocationManager)); - } - } - // ACTION STOP - else if (ACTION_STOP.equals(intent.getAction())) { - stopTracking(); - } - // ACTION RESUME - else if (ACTION_RESUME.equals(intent.getAction())) { - resumeTracking(LocationHelper.determineLastKnownLocation(mLocationManager)); - } - - // START_STICKY is used for services that are explicitly started and stopped as needed - return START_STICKY; - } - - - @Override - public void onTaskRemoved(Intent rootIntent) { - super.onTaskRemoved(rootIntent); - LogHelper.v(LOG_TAG, "onTaskRemoved called."); - } - - - @Override - public void onDestroy() { - LogHelper.v(LOG_TAG, "onDestroy called."); - - if (mTrackerServiceRunning) { - stopTracking(); - } - - // remove TrackerService from foreground state - stopForeground(true); - - super.onDestroy(); - } - - - @Override - public void onSensorChanged(SensorEvent sensorEvent) { - // save the step count offset (steps previously recorded by the system) and subtract any steps recorded during this session in case the app was killed - if (mStepCountOffset == 0) { - mStepCountOffset = (sensorEvent.values[0] - 1) - mTrack.getStepCount(); - } - - // calculate step count - float stepCount = sensorEvent.values[0] - mStepCountOffset; - - // set step count in track - mTrack.setStepCount(stepCount); - } - - - @Override - public void onAccuracyChanged(Sensor sensor, int i) { - - } - - - /* Start tracking location */ - public void startTracking(Location lastLocation) { - if (mLocationSystemSetting) { - LogHelper.v(LOG_TAG, "Start tracking"); - - // create a new track - if requested - mTrack = new Track(); - - // get last location - if (lastLocation != null) { - mCurrentBestLocation = lastLocation; - } else { - mCurrentBestLocation = LocationHelper.determineLastKnownLocation(mLocationManager); - } - - // begin recording - startMovementRecording(); - - } else { - LogHelper.i(LOG_TAG, "Location Setting is turned off."); - Toast.makeText(getApplicationContext(), R.string.toast_message_location_offline, Toast.LENGTH_LONG).show(); - } - } - - - /* Resume tracking after stop/pause */ - public void resumeTracking(Location lastLocation) { - if (mLocationSystemSetting) { - LogHelper.v(LOG_TAG, "Recording resumed"); - - // switch the resume flag - mResumedFlag = true; - - // create a new track - if requested - StorageHelper storageHelper = new StorageHelper(this); - if (storageHelper.tempFileExists()) { - // load temp track file - mTrack = storageHelper.loadTrack(FILE_TEMP_TRACK); - // try to mark last waypoint as stopover - int lastWayPoint = mTrack.getSize() - 1; - if (lastWayPoint >= 0) { - mTrack.getWayPoints().get(lastWayPoint).setIsStopOver(true); - } - } else { - // fallback, if tempfile did not exist - LogHelper.e(LOG_TAG, "Unable to find previously saved track temp file."); - mTrack = new Track(); - } - - // get last location - mCurrentBestLocation = lastLocation; - // FALLBACK: use last recorded location - if (mCurrentBestLocation == null && mTrack.getSize() > 0) { - mCurrentBestLocation = mTrack.getWayPointLocation(mTrack.getSize() -1); - } - - // begin recording - startMovementRecording(); - - } else { - LogHelper.i(LOG_TAG, "Location Setting is turned off."); - Toast.makeText(getApplicationContext(), R.string.toast_message_location_offline, Toast.LENGTH_LONG).show(); - } - } - - - /* Stop tracking location */ - public void stopTracking() { - LogHelper.v(LOG_TAG, "Recording stopped"); - - // catches a bug that leaves the ui in a incorrect state after a crash - if (!mTrackerServiceRunning) { - saveTrackerServiceState(mTrackerServiceRunning, FAB_STATE_SAVE); - broadcastTrackingStateChange(); - return; - } - - // store current date and time - mTrack.setRecordingEnd(); - - // stop timer - mTimer.cancel(); - - // broadcast an updated track - broadcastTrackUpdate(); - - // save a temp file in case the activity has been killed - SaveTempTrackAsyncHelper saveTempTrackAsyncHelper = new SaveTempTrackAsyncHelper(); - saveTempTrackAsyncHelper.execute(); - - // change notification - displayNotification(false); - - // reset resume flag - mResumedFlag = false; - - // remove listeners - stopFindingLocation(); - mSensorManager.unregisterListener(this); - - // disable content observer for changes in System Settings - this.getContentResolver().unregisterContentObserver(mSettingsContentObserver); - - // remove TrackerService from foreground state - stopForeground(false); - } - - - /* Dismiss notification */ - public void dismissNotification() { - // save state - saveTrackerServiceState(mTrackerServiceRunning, FAB_STATE_DEFAULT); - // cancel notification - mNotificationManager.cancel(TRACKER_SERVICE_NOTIFICATION_ID); // todo check if necessary? - stopForeground(true); - } - - - /* Starts to record movements */ - private void startMovementRecording() { - // initialize step counter - mStepCountOffset = 0; - - // add last location as WayPoint to track - addWayPointToTrack(); - - // put up notification - displayNotification(true); - - // create gps and network location listeners - startFindingLocation(); - - // start timer that periodically request a location update - startRequestingLocationChanges(); - - // start counting steps - startStepCounter(); - - // register content observer for changes in System Settings - this.getContentResolver().registerContentObserver(android.provider.Settings.Secure.CONTENT_URI, true, mSettingsContentObserver); - - // start service in foreground - startForeground(TRACKER_SERVICE_NOTIFICATION_ID, mNotification); - } - - - /* Registers a step counter listener */ - private void startStepCounter() { - boolean stepCounterAvailable = mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(TYPE_STEP_COUNTER), SensorManager.SENSOR_DELAY_UI); - if (stepCounterAvailable) { - LogHelper.v(LOG_TAG, "Pedometer sensor available: Registering listener."); - } else { - LogHelper.i(LOG_TAG, "Pedometer sensor not available."); - mTrack.setStepCount(-1); - } - } - - - /* Set timer to periodically retrieve new locations and to prevent endless tracking */ - private void startRequestingLocationChanges() { - final long previouslyRecordedDuration = mTrack.getTrackDuration(); - mTimer = new CountDownTimer(EIGHT_HOURS_IN_MILLISECONDS, FIFTEEN_SECONDS_IN_MILLISECONDS) { - @Override - public void onTick(long millisUntilFinished) { - // update track duration - and add duration from previously interrupted / paused session - long duration = EIGHT_HOURS_IN_MILLISECONDS - millisUntilFinished + previouslyRecordedDuration; - mTrack.setDuration(duration); - // try to add WayPoint to Track - addWayPointToTrack(); - // update notification - mNotification = NotificationHelper.getUpdatedNotification(TrackerService.this, mNotificationBuilder, mTrack); - mNotificationManager.notify(TRACKER_SERVICE_NOTIFICATION_ID, mNotification); - // save a temp file in case the service has been killed by the system - SaveTempTrackAsyncHelper saveTempTrackAsyncHelper = new SaveTempTrackAsyncHelper(); - saveTempTrackAsyncHelper.execute(); - } - - @Override - public void onFinish() { - // stop tracking after eight hours - stopTracking(); - } - }; - mTimer.start(); - } - - - /* Display notification */ - private void displayNotification(boolean trackingState) { - mNotificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID_RECORDING_CHANNEL); - mNotification = NotificationHelper.getNotification(this, mNotificationBuilder, mTrack, trackingState); - mNotificationManager.notify(TRACKER_SERVICE_NOTIFICATION_ID, mNotification); // todo check if necessary in pre Android O - } - - - /* Adds a new WayPoint to current track */ - private void addWayPointToTrack() { - - boolean success = false; - Location previousLocation = null; - int trackSize = mTrack.getSize(); - - if (trackSize == 0) { - // if accurate AND current - if (LocationHelper.isAccurate(mCurrentBestLocation) && LocationHelper.isCurrent(mCurrentBestLocation)) { - // add first location to track - success = mTrack.addWayPoint(previousLocation, mCurrentBestLocation); - } else { - // just send a broadcast indicating that current location fix not not suited - broadcastTrackUpdate(); - } - } else { - // get location of previous WayPoint - previousLocation = mTrack.getWayPointLocation(trackSize - 1); - - // default value for average speed - float averageSpeed = 0f; - - // compute average speed if new location came from network provider - if (trackSize > 1 && LocationManager.NETWORK_PROVIDER.equals(mCurrentBestLocation.getProvider())) { - Location firstWayPoint = mTrack.getWayPointLocation(0); - float distance = firstWayPoint.distanceTo(previousLocation); - long timeDifference = previousLocation.getElapsedRealtimeNanos() - firstWayPoint.getElapsedRealtimeNanos(); - averageSpeed = distance / ((float) timeDifference / ONE_SECOND_IN_NANOSECOND); - } - - // if accurate AND new - if (LocationHelper.isAccurate(mCurrentBestLocation) && LocationHelper.isNewWayPoint(previousLocation, mCurrentBestLocation, averageSpeed)) { - // add current best location to track - success = mTrack.addWayPoint(previousLocation, mCurrentBestLocation); - } - } - - if (success) { - if (mResumedFlag) { - int lastWayPoint = mTrack.getSize() - 2; - if (lastWayPoint >= 0) { - // mark last location as stop over - mTrack.getWayPoints().get(lastWayPoint).setIsStopOver(true); - } - mResumedFlag = false; - } else { - // update distance, if not resumed - mTrack.updateDistance(previousLocation, mCurrentBestLocation); - } - - // send local broadcast if new WayPoint was added - broadcastTrackUpdate(); - } - - } - - - /* Broadcasts a track update */ - private void broadcastTrackUpdate() { - if (mTrack != null) { - Intent i = new Intent(); - i.setAction(ACTION_TRACK_UPDATED); - i.putExtra(EXTRA_TRACK, mTrack); - i.putExtra(EXTRA_LAST_LOCATION, mCurrentBestLocation); - LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(i); - } - } - - - /* Creates a location listener */ - private LocationListener createLocationListener() { - return new LocationListener() { - public void onLocationChanged(Location location) { - // check if the new location is better - if (LocationHelper.isBetterLocation(location, mCurrentBestLocation)) { - // save location - mCurrentBestLocation = location; - } - } - - public void onStatusChanged(String provider, int status, Bundle extras) { - LogHelper.v(LOG_TAG, "Location provider status change: " + provider + " | " + status); - } - - public void onProviderEnabled(String provider) { - LogHelper.v(LOG_TAG, "Location provider enabled: " + provider); - } - - public void onProviderDisabled(String provider) { - LogHelper.v(LOG_TAG, "Location provider disabled: " + provider); - } - }; - } - - - /* Creates gps and network location listeners */ - private void startFindingLocation() { - - // register location listeners and request updates - List locationProviders = mLocationManager.getAllProviders(); - if (locationProviders.contains(LocationManager.GPS_PROVIDER)) { - mGPSListener = createLocationListener(); - mTrackerServiceRunning = true; - } - if (locationProviders.contains(LocationManager.NETWORK_PROVIDER)) { - mNetworkListener = createLocationListener(); - mTrackerServiceRunning = true; - } - LocationHelper.registerLocationListeners(mLocationManager, mGPSListener, mNetworkListener); - saveTrackerServiceState(mTrackerServiceRunning, FAB_STATE_RECORDING); - - // notify MainActivity - broadcastTrackingStateChange(); - } - - - /* Removes gps and network location listeners */ - private void stopFindingLocation() { - // remove listeners - LocationHelper.removeLocationListeners(mLocationManager, mGPSListener, mNetworkListener); - mTrackerServiceRunning = false; - saveTrackerServiceState(mTrackerServiceRunning, FAB_STATE_SAVE); - - // notify MainActivity - broadcastTrackingStateChange(); - } - - - /* Sends a broadcast with tracking changed */ - private void broadcastTrackingStateChange() { - Intent i = new Intent(); - i.setAction(ACTION_TRACKING_STATE_CHANGED); - i.putExtra(EXTRA_TRACK, mTrack); - i.putExtra(EXTRA_LAST_LOCATION, mCurrentBestLocation); - i.putExtra(EXTRA_TRACKING_STATE, mTrackerServiceRunning); - LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(i); - } - - - /* Saves state of Tracker Service and floating Action Button */ - private void saveTrackerServiceState(boolean trackerServiceRunning, int fabState) { - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); - SharedPreferences.Editor editor = settings.edit(); - editor.putBoolean(PREFS_TRACKER_SERVICE_RUNNING, trackerServiceRunning); - editor.putInt(PREFS_FAB_STATE, fabState); - editor.apply(); - } - - - /** - * Inner class: Local Binder that returns this service - */ - public class LocalBinder extends Binder { - TrackerService getService() { - // return this instance of TrackerService so clients can call public methods - return TrackerService.this; - } - } - /** - * End of inner class - */ - - - /** - * Inner class: SettingsContentObserver is a custom ContentObserver for changes in Android Settings - */ - public class SettingsContentObserver extends ContentObserver { - - public SettingsContentObserver(Handler handler) { - super(handler); - } - - @Override - public boolean deliverSelfNotifications() { - return super.deliverSelfNotifications(); - } - - @Override - public void onChange(boolean selfChange) { - super.onChange(selfChange); - LogHelper.v(LOG_TAG, "System Setting change detected."); - - // check if location setting was changed - boolean previousLocationSystemSetting = mLocationSystemSetting; - mLocationSystemSetting = LocationHelper.checkLocationSystemSetting(getApplicationContext()); - if (previousLocationSystemSetting != mLocationSystemSetting && !mLocationSystemSetting && mTrackerServiceRunning) { - LogHelper.v(LOG_TAG, "Location Setting turned off while tracking service running."); - if (mTrack != null) { - stopTracking(); - } - stopForeground(true); - } - } - - } - /** - * End of inner class - */ - - - /** - * Inner class: Saves track to external storage using AsyncTask - */ - private class SaveTempTrackAsyncHelper extends AsyncTask { - - @Override - protected Void doInBackground(Void... voids) { - LogHelper.v(LOG_TAG, "Saving temporary track object in background."); - // save track object - StorageHelper storageHelper = new StorageHelper(TrackerService.this); - storageHelper.saveTrack(mTrack, FILE_TEMP_TRACK); - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - super.onPostExecute(aVoid); - LogHelper.v(LOG_TAG, "Saving finished."); - } - } - /** - * End of inner class - */ - -} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/TrackerService.kt b/app/src/main/java/org/y20k/trackbook/TrackerService.kt new file mode 100644 index 0000000..22098b3 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/TrackerService.kt @@ -0,0 +1,356 @@ +/* + * TrackerService.kt + * Implements the app's movement tracker service + * The TrackerService keeps track of the current location + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook + +import android.Manifest +import android.app.Notification +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Binder +import android.os.Bundle +import android.os.Handler +import android.os.IBinder +import androidx.core.content.ContextCompat +import androidx.preference.PreferenceManager +import kotlinx.coroutines.* +import org.y20k.trackbook.core.Track +import org.y20k.trackbook.helpers.* +import java.util.* +import kotlin.coroutines.CoroutineContext + + +/* + * TrackerService class + */ +class TrackerService(): Service(), CoroutineScope, SensorEventListener { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(TrackerService::class.java) + + + /* Main class variables */ + var trackingState: Int = Keys.STATE_NOT_TRACKING + var gpsProviderActive: Boolean = false + var networkProviderActive: Boolean = false + var useImperial: Boolean = false + var gpsOnly: Boolean = false + var locationAccuracyThreshold: Int = Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY + var currentBestLocation: Location = LocationHelper.getDefaultLocation() + var stepCountOffset: Float = 0f + var track: Track = Track() + private val binder = LocalBinder() + private val handler: Handler = Handler() + private lateinit var locationManager: LocationManager + private lateinit var sensorManager: SensorManager + private lateinit var notificationManager: NotificationManager + private lateinit var notificationHelper: NotificationHelper + private lateinit var gpsLocationListener: LocationListener + private lateinit var networkLocationListener: LocationListener + private lateinit var backgroundJob: Job + + + /* Overrides coroutineContext variable */ + override val coroutineContext: CoroutineContext get() = backgroundJob + Dispatchers.Main + + + /* Overrides onCreate from Service */ + override fun onCreate() { + super.onCreate() + locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager + sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager + notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationHelper = NotificationHelper(this) + gpsProviderActive = LocationHelper.isGpsEnabled(locationManager) + networkProviderActive = LocationHelper.isNetworkEnabled(locationManager) + gpsLocationListener = createLocationListener() + networkLocationListener = createLocationListener() + useImperial = PreferencesHelper.loadUseImperialUnits(this) + locationAccuracyThreshold = PreferencesHelper.loadAccuracyThreshold(this) + trackingState = PreferencesHelper.loadTrackingState(this) + currentBestLocation = LocationHelper.getLastKnownLocation(this) + track = FileHelper.readTrack(this, FileHelper.getTempFileUri(this)) + backgroundJob = Job() + PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener) + } + + + /* Overrides onStartCommand from Service */ + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + + // SERVICE RESTART (via START_STICKY) + if (intent == null) { + if (trackingState == Keys.STATE_TRACKING_ACTIVE) { + LogHelper.w(TAG, "Trackbook has been killed by the operating system. Trying to resume recording.") + resumeTracking() + } + // ACTION STOP + } else if (Keys.ACTION_STOP == intent.action) { + stopTracking() + // ACTION START + } else if (Keys.ACTION_START == intent.action) { + startTracking() + // ACTION RESUME + } else if (Keys.ACTION_RESUME == intent.action) { + resumeTracking() + } + + // START_STICKY is used for services that are explicitly started and stopped as needed + return Service.START_STICKY + } + + + /* Overrides onBind from Service */ + override fun onBind(p0: Intent?): IBinder? { + addLocationListeners() + return binder + } + + + /* Overrides onDestroy from Service */ + override fun onDestroy() { + super.onDestroy() + LogHelper.i(TAG, "onDestroy called.") + if (trackingState == Keys.STATE_TRACKING_ACTIVE) stopTracking() + stopForeground(true) + PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener) + removeLocationListeners() + backgroundJob.cancel() + } + + + /* Overrides onAccuracyChanged from SensorEventListener */ + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + LogHelper.v(TAG, "Accuracy changed: $accuracy") + } + + + /* Overrides onSensorChanged from SensorEventListener */ + override fun onSensorChanged(sensorEvent: SensorEvent?) { + var steps: Float = 0f + if (sensorEvent != null) { + if (stepCountOffset == 0f) { + // store steps previously recorded by the system + stepCountOffset = (sensorEvent.values[0] - 1) - track.stepCount // subtract any steps recorded during this session in case the app was killed + } + // calculate step count - subtract steps previously recorded + steps = sensorEvent.values[0] - stepCountOffset + } + // update step count in track + track.stepCount = steps + } + + + /* Resume tracking after stop/pause */ + fun resumeTracking() { + // load temp track - returns an empty track if not available + track = FileHelper.readTrack(this, FileHelper.getTempFileUri(this)) + // try to mark last waypoint as stopover + if (track.wayPoints.size > 0) { + val lastWayPointIndex = track.wayPoints.size - 1 + track.wayPoints.get(lastWayPointIndex).isStopOver = true + } + // start tracking + startTracking(newTrack = false) + } + + + /* Start tracking location */ + fun startTracking(newTrack: Boolean = true) { + if (newTrack) { + track.recordingStart = GregorianCalendar.getInstance().time + track.recordingStop = track.recordingStart + track.name = DateTimeHelper.convertToReadableDate(track.recordingStart) + stepCountOffset = 0f + } + trackingState = Keys.STATE_TRACKING_ACTIVE + PreferencesHelper.saveTrackingState(this, trackingState) + startStepCounter() + handler.postDelayed(periodicTrackUpdate, 0) + startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification()) + } + + + /* Stop tracking location */ + fun stopTracking() { + track.recordingStop = GregorianCalendar.getInstance().time + trackingState = Keys.STATE_TRACKING_STOPPED + PreferencesHelper.saveTrackingState(this, trackingState) + sensorManager.unregisterListener(this) + handler.removeCallbacks(periodicTrackUpdate) + displayNotification() + stopForeground(false) + } + + + /* Clear track recording */ + fun clearTrack() { + track = Track() + FileHelper.deleteTempFile(this) + trackingState = Keys.STATE_NOT_TRACKING + PreferencesHelper.saveTrackingState(this, trackingState) + stopForeground(true) + } + + + /* Saves track recording to storage */ + fun saveTrack() { + // save track using "deferred await" + launch { + // step 1: create and store filenames for json and gpx files + track.trackUriString = FileHelper.getTrackFileUri(this@TrackerService, track).toString() + track.gpxUriString = FileHelper.getGpxFileUri(this@TrackerService, track).toString() + // step 2: save track + FileHelper.saveTrackSuspended(track, saveGpxToo = true) + // step 3: save tracklist + FileHelper.addTrackAndSaveTracklistSuspended(this@TrackerService, track) + // step 3: clear track + clearTrack() + } + } + + + /* Creates location listener */ + private fun createLocationListener(): LocationListener { + return object : LocationListener { + override fun onLocationChanged(location: Location) { + // update currentBestLocation if a better location is available + if (LocationHelper.isBetterLocation(location, currentBestLocation)) { + currentBestLocation = location + } + } + override fun onProviderEnabled(provider: String) { + LogHelper.v(TAG, "onProviderEnabled $provider") + when (provider) { + LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled(locationManager) + LocationManager.NETWORK_PROVIDER -> networkProviderActive = LocationHelper.isNetworkEnabled(locationManager) + } + } + override fun onProviderDisabled(provider: String) { + LogHelper.v(TAG, "onProviderDisabled $provider") + when (provider) { + LocationManager.GPS_PROVIDER -> gpsProviderActive = LocationHelper.isGpsEnabled(locationManager) + LocationManager.NETWORK_PROVIDER -> networkProviderActive = LocationHelper.isNetworkEnabled(locationManager) + } + } + override fun onStatusChanged(p0: String?, p1: Int, p2: Bundle?) { + // deprecated method + } + } + } + + + /* Adds location listeners to location manager */ + private fun addLocationListeners() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + if (gpsProviderActive) { + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f,gpsLocationListener) + } + if (networkProviderActive) { + locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0f,networkLocationListener) + } + } else { + LogHelper.w(TAG, "Unable to request device location. Permission is not granted.") + } + } + + + /* Removes location listeners from location manager */ + private fun removeLocationListeners() { + locationManager.removeUpdates(gpsLocationListener) + locationManager.removeUpdates(networkLocationListener) + } + + + /* Registers a step counter listener */ + private fun startStepCounter() { + val stepCounterAvailable = sensorManager.registerListener(this, sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER), SensorManager.SENSOR_DELAY_UI) + if (!stepCounterAvailable) { + LogHelper.w(TAG, "Pedometer sensor not available.") + track.stepCount = -1f + } + } + + + /* Displays / updates notification */ + private fun displayNotification(): Notification { + val notification: Notification = notificationHelper.createNotification(trackingState, track.length, track.duration, useImperial) + notificationManager.notify(Keys.TRACKER_SERVICE_NOTIFICATION_ID, notification) + return notification + } + + + /* + * Defines the listener for changes in shared preferences + */ + val sharedPreferenceChangeListener = object : SharedPreferences.OnSharedPreferenceChangeListener { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + Keys.PREF_GPS_ONLY -> gpsOnly = PreferencesHelper.loadGpsOnly(this@TrackerService) + Keys.PREF_USE_IMPERIAL_UNITS -> useImperial = PreferencesHelper.loadUseImperialUnits(this@TrackerService) + Keys.PREF_LOCATION_ACCURACY_THRESHOLD -> locationAccuracyThreshold = PreferencesHelper.loadAccuracyThreshold(this@TrackerService) + } + } + } + /* + * End of declaration + */ + + + /* + * Inner class: Local Binder that returns this service + */ + inner class LocalBinder : Binder() { + val service: TrackerService = this@TrackerService + } + /* + * End of inner class + */ + + + /* + * Runnable: Periodically track updates (if recording active) + */ + private val periodicTrackUpdate: Runnable = object : Runnable { + override fun run() { + // add waypoint to track - step count is continuously updated in onSensorChanged + track = TrackHelper.addWayPointToTrack(track, currentBestLocation, locationAccuracyThreshold) + // update notification + displayNotification() + // save temp track using GlobalScope.launch = fire & forget (no return value from save) + GlobalScope.launch { FileHelper.saveTempTrackSuspended(this@TrackerService, track) } + // re-run this in 10 seconds + handler.postDelayed(this, Keys.ADD_WAYPOINT_TO_TRACK_INTERVAL) + } + } + /* + * End of declaration + */ + + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/TracklistFragment.kt b/app/src/main/java/org/y20k/trackbook/TracklistFragment.kt new file mode 100644 index 0000000..4c52350 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/TracklistFragment.kt @@ -0,0 +1,161 @@ +/* + * TracklistFragment.kt + * Implements the TracklistFragment fragment + * A TracklistFragment displays a list recorded tracks + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook + +import YesNoDialog +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DefaultItemAnimator +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import org.y20k.escapepod.helpers.UiHelper +import org.y20k.trackbook.core.TracklistElement +import org.y20k.trackbook.helpers.LogHelper +import org.y20k.trackbook.helpers.TrackHelper +import org.y20k.trackbook.tracklist.TracklistAdapter + + +/* + * TracklistFragment class + */ +class TracklistFragment : Fragment(), TracklistAdapter.TracklistAdapterListener, YesNoDialog.YesNoDialogListener { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(TracklistFragment::class.java) + + + /* Main class variables */ + private lateinit var tracklistAdapter: TracklistAdapter + private lateinit var trackElementList: RecyclerView + private lateinit var tracklistOnboarding: ConstraintLayout + + + /* Overrides onCreateView from Fragment */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // create tracklist adapter + tracklistAdapter = TracklistAdapter(this) + } + + + /* Overrides onCreateView from Fragment */ + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + // find views + val rootView = inflater.inflate(R.layout.fragment_tracklist, container, false) + trackElementList = rootView.findViewById(R.id.track_element_list) + tracklistOnboarding = rootView.findViewById(R.id.track_list_onboarding) + + // set up recycler view + trackElementList.layoutManager = CustomLinearLayoutManager(activity as Context) + trackElementList.itemAnimator = DefaultItemAnimator() + trackElementList.adapter = tracklistAdapter + + // enable swipe to delete + val swipeHandler = object : UiHelper.SwipeToDeleteCallback(activity as Context) { + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + // ask user + val adapterPosition: Int = viewHolder.adapterPosition + val dialogMessage: String = "${getString(R.string.dialog_yes_no_message_remove_recording)}\n\n- ${tracklistAdapter.getTrackName(adapterPosition)}" + YesNoDialog(this@TracklistFragment as YesNoDialog.YesNoDialogListener).show(context = activity as Context, type = Keys.DIALOG_REMOVE_TRACK, messageString = dialogMessage, yesButton = R.string.dialog_yes_no_positive_button_remove_recording, payload = adapterPosition) + } + } + val itemTouchHelper = ItemTouchHelper(swipeHandler) + itemTouchHelper.attachToRecyclerView(rootView.findViewById(R.id.track_element_list)) + + // toggle onboarding layout + toggleOnboardingLayout(tracklistAdapter.itemCount) + + return rootView + } + + + /* Overrides onTrackElementTapped from TracklistElementAdapterListener */ + override fun onTrackElementTapped(tracklistElement: TracklistElement) { + val bundle: Bundle = Bundle() + bundle.putString(Keys.ARG_TRACK_TITLE, tracklistElement.name) + bundle.putString(Keys.ARG_TRACK_FILE_URI, tracklistElement.trackUriString) + bundle.putString(Keys.ARG_GPX_FILE_URI, tracklistElement.gpxUriString) + bundle.putLong(Keys.ARG_TRACK_ID, TrackHelper.getTrackId(tracklistElement)) + findNavController().navigate(R.id.fragment_track, bundle) + } + + + /* Overrides onYesNoDialog from YesNoDialogListener */ + override fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) { + when (type) { + Keys.DIALOG_REMOVE_TRACK -> { + when (dialogResult) { + // user tapped remove track + true -> { + toggleOnboardingLayout(tracklistAdapter.itemCount -1) + tracklistAdapter.removeTrack(activity as Context, payload) + } + // user tapped cancel + false -> { + tracklistAdapter.notifyItemChanged(payload) + } + } + } + } + } + + + // toggle onboarding layout + private fun toggleOnboardingLayout(trackCount: Int) { + when (trackCount == 0) { + true -> tracklistOnboarding.visibility = View.VISIBLE // show onboarding layout + false -> tracklistOnboarding.visibility = View.GONE // hide onboarding layout + } + } + + + + /* + * Inner class: custom LinearLayoutManager that overrides onLayoutCompleted + */ + inner class CustomLinearLayoutManager(context: Context): LinearLayoutManager(context, VERTICAL, false) { + + override fun supportsPredictiveItemAnimations(): Boolean { + return true + } + + override fun onLayoutCompleted(state: RecyclerView.State?) { + super.onLayoutCompleted(state) + // handle delete request from TrackFragment - after layout calculations are complete + val deleteTrackId: Long = arguments?.getLong(Keys.ARG_TRACK_ID, -1L) ?: -1L + arguments?.putLong(Keys.ARG_TRACK_ID, -1L) + if (deleteTrackId != -1L) { + val position: Int = tracklistAdapter.findPosition(deleteTrackId) + tracklistAdapter.removeTrack(this@TracklistFragment.activity as Context, position) + toggleOnboardingLayout(tracklistAdapter.itemCount -1) + } + } + + } + /* + * End of inner class + */ + +} diff --git a/app/src/main/java/org/y20k/trackbook/core/Track.java b/app/src/main/java/org/y20k/trackbook/core/Track.java deleted file mode 100755 index 6e0183b..0000000 --- a/app/src/main/java/org/y20k/trackbook/core/Track.java +++ /dev/null @@ -1,337 +0,0 @@ -/** - * Track.java - * Implements the Track class - * A Track stores a list of WayPoints - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.core; - -import android.location.Location; -import android.os.Parcel; -import android.os.Parcelable; - -import androidx.annotation.Nullable; - -import org.osmdroid.util.BoundingBox; -import org.y20k.trackbook.helpers.LocationHelper; -import org.y20k.trackbook.helpers.TrackbookKeys; - -import java.util.ArrayList; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.List; - - -/** - * Track class - */ -public class Track implements TrackbookKeys, Parcelable { - - /* Define log tag */ - private static final String LOG_TAG = Track.class.getSimpleName(); - - - /* Main class variables */ - private final int mTrackFormatVersion; - private final List mWayPoints; - private float mTrackLength; - private long mDuration; - private float mStepCount; - private final Date mRecordingStart; - private Date mRecordingStop; - private double mMaxAltitude; - private double mMinAltitude; - private double mPositiveElevation; - private double mNegativeElevation; - private BoundingBox mBoundingBox; - - - /* Generic Constructor */ - public Track(int trackFormatVersion, List wayPoints, float trackLength, long duration, float stepCount, Date recordingStart, Date recordingStop, double maxAltitude, double minAltitude, double positiveElevation, double negativeElevation, BoundingBox boundingBox) { - mTrackFormatVersion = trackFormatVersion; - mWayPoints = wayPoints; - mTrackLength = trackLength; - mDuration = duration; - mStepCount = stepCount; - mRecordingStart = recordingStart; - mRecordingStop = recordingStop; - mMaxAltitude = maxAltitude; - mMinAltitude = minAltitude; - mPositiveElevation = positiveElevation; - mNegativeElevation = negativeElevation; - mBoundingBox = boundingBox; - } - - - /* Copy Constructor */ - public Track(Track track) { - this(track.getTrackFormatVersion(), track.getWayPoints(), track.getTrackLength(), track.getTrackDuration(), track.getStepCount(), track.getRecordingStart(), track.getRecordingStop(), track.getMaxAltitude(), track.getMinAltitude(), track.getPositiveElevation(), track.getNegativeElevation(), track.getBoundingBox()); - } - - - /* Constructor */ - public Track() { - mTrackFormatVersion = CURRENT_TRACK_FORMAT_VERSION; - mWayPoints = new ArrayList(); - mTrackLength = 0f; - mDuration = 0; - mStepCount = 0f; - mRecordingStart = GregorianCalendar.getInstance().getTime(); - mRecordingStop = mRecordingStart; - mMaxAltitude = 0f; - mMinAltitude = 0f; - mPositiveElevation = 0f; - mNegativeElevation = 0f; - mBoundingBox = new BoundingBox(); - } - - - /* Constructor used by CREATOR */ - protected Track(Parcel in) { - mTrackFormatVersion = in.readInt(); - mWayPoints = in.createTypedArrayList(WayPoint.CREATOR); - mTrackLength = in.readFloat(); - mDuration = in.readLong(); - mStepCount = in.readFloat(); - mRecordingStart = new Date(in.readLong()); - mRecordingStop = new Date(in.readLong()); - mMaxAltitude = in.readDouble(); - mMinAltitude = in.readDouble(); - mPositiveElevation = in.readDouble(); - mNegativeElevation = in.readDouble(); - mBoundingBox = new BoundingBox(in.readDouble(), in.readDouble(),in.readDouble(),in.readDouble()); - // BoundingBox(double north, double east, double south, double west) - } - - - /* CREATOR for Track object used to do parcel related operations */ - public static final Creator CREATOR = new Creator() { - @Override - public Track createFromParcel(Parcel in) { - return new Track(in); - } - - @Override - public Track[] newArray(int size) { - return new Track[size]; - } - }; - - - /* Adds new WayPoint */ - public boolean addWayPoint(@Nullable Location previousLocation, Location newLocation) { - - // toggle stop over status, if necessary - boolean isStopOver = LocationHelper.isStopOver(previousLocation, newLocation); - if (isStopOver) { - int wayPointCount = mWayPoints.size(); - mWayPoints.get(wayPointCount-1).setIsStopOver(isStopOver); - } - - // create new WayPoint - WayPoint wayPoint = new WayPoint(newLocation, false, mTrackLength); - - // add new WayPoint to track - return mWayPoints.add(wayPoint); - } - - - /* Updates distance */ - public boolean updateDistance(@Nullable Location previousLocation, Location newLocation){ - // two data points needed to calculate distance - if (previousLocation != null) { - // add up distance - mTrackLength = mTrackLength + previousLocation.distanceTo(newLocation); - return true; - } else { - // this was the first waypoint - return false; - } - } - - - /* Toggles stop over status of last waypoint */ - public void toggleLastWayPointStopOverStatus(boolean stopOver) { - int wayPointCount = mWayPoints.size(); - mWayPoints.get(wayPointCount-1).setIsStopOver(stopOver); - } - - - /* Sets end time and date of recording */ - public void setRecordingEnd() { - mRecordingStop = GregorianCalendar.getInstance().getTime(); - } - - - /* Setter for duration of track */ - public void setDuration(long duration) { - mDuration = duration; - } - - - /* Setter for step count of track */ - public void setStepCount(float stepCount) { - mStepCount = stepCount; - } - - - /* Setter for maximum altitude of recording */ - public void setMaxAltitude(double maxAltitude) { - mMaxAltitude = maxAltitude; - } - - - /* Setter for lowest altitude of recording */ - public void setMinAltitude(double minAltitude) { - mMinAltitude = minAltitude; - } - - - /* Setter for positive elevation of recording (cumulative altitude difference) */ - public void setPositiveElevation(double positiveElevation) { - mPositiveElevation = positiveElevation; - } - - - /* Setter for negative elevation of recording (cumulative altitude difference) */ - public void setNegativeElevation(double negativeElevation) { - mNegativeElevation = negativeElevation; - } - - - /* Setter for this track's BoundingBox - a data structure describing the edge coordinates of a track */ - public void setBoundingBox(BoundingBox boundingBox) { - mBoundingBox = boundingBox; - } - - - /* Getter for file/track format version */ - public int getTrackFormatVersion() { - return mTrackFormatVersion; - } - - - /* Getter for mWayPoints */ - public List getWayPoints() { - return mWayPoints; - } - - - /* Getter size of Track / number of WayPoints */ - public int getSize() { - return mWayPoints.size(); - } - - - /* Getter for track length */ - public float getTrackLength() { - return mTrackLength; - } - - - /* Getter for duration of track */ - public long getTrackDuration() { - return mDuration; - } - - /* Getter for start date of recording */ - public Date getRecordingStart() { - return mRecordingStart; - } - - - /* Getter for stop date of recording */ - public Date getRecordingStop() { - return mRecordingStop; - } - - - /* Getter for step count of recording */ - public float getStepCount() { - return mStepCount; - } - - - /* Getter for maximum altitude of recording */ - public double getMaxAltitude() { - return mMaxAltitude; - } - - - /* Getter for lowest altitude of recording */ - public double getMinAltitude() { - return mMinAltitude; - } - - - /* Getter for positive elevation of recording (cumulative altitude difference) */ - public double getPositiveElevation() { - return mPositiveElevation; - } - - - /* Getter for negative elevation of recording (cumulative altitude difference) */ - public double getNegativeElevation() { - return mNegativeElevation; - } - - - /* Getter for this track's BoundingBox - a data structure describing the edge coordinates of a track */ - public BoundingBox getBoundingBox() { return mBoundingBox; } - - - /* Getter recorded distance */ - public Double getTrackDistance() { - int size = mWayPoints.size(); - if (size > 0) { - return (double)mWayPoints.get(size - 1).getDistanceToStartingPoint(); - } else { - return (double)0f; - } - } - - - /* Getter for location of specific WayPoint */ - public Location getWayPointLocation(int index) { - return mWayPoints.get(index).getLocation(); - } - - - @Override - public int describeContents() { - return 0; - } - - - @Override - public void writeToParcel(Parcel parcel, int flags) { - parcel.writeInt(mTrackFormatVersion); - parcel.writeTypedList(mWayPoints); - parcel.writeFloat(mTrackLength); - parcel.writeLong(mDuration); - parcel.writeFloat(mStepCount); - parcel.writeLong(mRecordingStart.getTime()); - parcel.writeLong(mRecordingStop.getTime()); - parcel.writeDouble(mMaxAltitude); - parcel.writeDouble(mMinAltitude); - parcel.writeDouble(mPositiveElevation); - parcel.writeDouble(mNegativeElevation); - parcel.writeDouble(mBoundingBox.getLatNorth()); - parcel.writeDouble(mBoundingBox.getLonEast()); - parcel.writeDouble(mBoundingBox.getLatSouth()); - parcel.writeDouble(mBoundingBox.getLonWest()); - // BoundingBox(double north, double east, double south, double west) - } - - -} diff --git a/app/src/main/java/org/y20k/trackbook/core/Track.kt b/app/src/main/java/org/y20k/trackbook/core/Track.kt new file mode 100644 index 0000000..7c566bd --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/core/Track.kt @@ -0,0 +1,77 @@ +/* + * Track.kt + * Implements the Track data class + * A Track stores a list of WayPoints + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.core + +import android.content.Context +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.Expose +import kotlinx.android.parcel.Parcelize +import org.y20k.trackbook.Keys +import org.y20k.trackbook.helpers.DateTimeHelper +import java.util.* + + +/* + * Track data class + */ +@Keep +@Parcelize +data class Track (@Expose var trackFormatVersion: Int = Keys.CURRENT_TRACK_FORMAT_VERSION, + @Expose val wayPoints: MutableList = mutableListOf(), + @Expose var length: Float = 0f, + @Expose var duration: Long = 0L, + @Expose var stepCount: Float = 0f, + @Expose var recordingStart: Date = GregorianCalendar.getInstance().time, + @Expose var recordingStop: Date = recordingStart, + @Expose var maxAltitude: Double = 0.0, + @Expose var minAltitude: Double = 0.0, + @Expose var positiveElevation: Double = 0.0, + @Expose var negativeElevation: Double = 0.0, + @Expose var trackUriString: String = String(), + @Expose var gpxUriString: String = String(), + @Expose var latitude: Double = Keys.DEFAULT_LATITUDE, + @Expose var longitude: Double = Keys.DEFAULT_LONGITUDE, + @Expose var zoomLevel: Double = Keys.DEFAULT_ZOOM_LEVEL, + @Expose var name: String = String()): Parcelable { + + + /* Creates a TracklistElement */ + fun toTracklistElement(context: Context): TracklistElement { + val readableDateString: String = DateTimeHelper.convertToReadableDate(recordingStart) + val readableDurationString: String = DateTimeHelper.convertToReadableTime(context, duration) + return TracklistElement( + name = name, + date = recordingStart, + dateString = readableDateString, + length = length, + durationString = readableDurationString, + trackUriString = trackUriString, + gpxUriString = gpxUriString, + starred = false + ) + } + + + /* Returns unique ID for Track - currently the start date */ + fun getTrackId(): Long { + return recordingStart.time + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/core/TrackBuilder.java b/app/src/main/java/org/y20k/trackbook/core/TrackBuilder.java deleted file mode 100755 index 73d2306..0000000 --- a/app/src/main/java/org/y20k/trackbook/core/TrackBuilder.java +++ /dev/null @@ -1,87 +0,0 @@ -/** - * TrackBuilder.java - * Implements a builder for the Track class - * A TrackBuilder can build a track object depending on the version of its file format - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - - -package org.y20k.trackbook.core; - -import org.osmdroid.util.BoundingBox; -import org.y20k.trackbook.helpers.LogHelper; -import org.y20k.trackbook.helpers.MapHelper; - -import java.util.Date; -import java.util.List; - - -/** - * TrackBuilder class - */ -public class TrackBuilder { - - /* Define log tag */ - private static final String LOG_TAG = TrackBuilder.class.getSimpleName(); - - - /* Main class variables */ - private final int mTrackFormatVersion; - private final List mWayPoints; - private final float mTrackLength; - private final long mDuration; - private final float mStepCount; - private final Date mRecordingStart; - private final Date mRecordingStop; - private final double mMaxAltitude; - private final double mMinAltitude; - private final double mPositiveElevation; - private final double mNegativeElevation; - private BoundingBox mBoundingBox; - - - /* Generic Constructor */ - public TrackBuilder(int trackFormatVersion, List wayPoints, float trackLength, long duration, float stepCount, Date recordingStart, Date recordingStop, double maxAltitude, double minAltitude, double positiveElevation, double negativeElevation, BoundingBox boundingBox) { - mTrackFormatVersion = trackFormatVersion; - mWayPoints = wayPoints; - mTrackLength = trackLength; - mDuration = duration; - mStepCount = stepCount; - mRecordingStart = recordingStart; - mRecordingStop = recordingStop; - mMaxAltitude = maxAltitude; - mMinAltitude = minAltitude; - mPositiveElevation = positiveElevation; - mNegativeElevation = negativeElevation; - mBoundingBox = boundingBox; - } - - - /* Builds and return a Track object */ - public Track toTrack() { - switch (mTrackFormatVersion) { - case 1: - // file format version 1 - does not have elevation data stored - return new Track(mTrackFormatVersion, mWayPoints, mTrackLength, mDuration, mStepCount, mRecordingStart, mRecordingStop, 0f, 0f, 0f, 0f, new BoundingBox()); - case 2: - // file format version 2 - does not have edge coordinates stored - return new Track(mTrackFormatVersion, mWayPoints, mTrackLength, mDuration, mStepCount, mRecordingStart, mRecordingStop, mMaxAltitude, mMinAltitude, mPositiveElevation, mNegativeElevation, MapHelper.calculateBoundingBox(mWayPoints)); - case 3: - // file format version 3 (current version) - return new Track(mTrackFormatVersion, mWayPoints, mTrackLength, mDuration, mStepCount, mRecordingStart, mRecordingStop, mMaxAltitude, mMinAltitude, mPositiveElevation, mNegativeElevation, new BoundingBox()); - default: - LogHelper.e(LOG_TAG, "Unknown file format version: " + mTrackFormatVersion); - return null; - } - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/core/TrackBundle.java b/app/src/main/java/org/y20k/trackbook/core/TrackBundle.java deleted file mode 100755 index 0f599df..0000000 --- a/app/src/main/java/org/y20k/trackbook/core/TrackBundle.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * TrackBundle.java - * Implements a TrackBundle - * TrackBundle is a container for file and corresponding name of a track - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - - -package org.y20k.trackbook.core; - -import org.y20k.trackbook.helpers.LogHelper; - -import java.io.File; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - - -/** - * TrackBundle class - */ -public class TrackBundle { - - /* Define log tag */ - private static final String LOG_TAG = TrackBundle.class.getSimpleName(); - - /* Main class variables */ - private final File mTrackFile; - private final String mTrackName; - - - /* Constructor */ - public TrackBundle(File file) { - mTrackFile = file; - mTrackName = buildTrackName(file); - } - - - /* Getter for track file */ - public File getTrackFile() { - return mTrackFile; - } - - - /* Getter for track name */ - public String getTrackName() { - return mTrackName; - } - - - /* Builds a readable track name from the track's file name */ - private String buildTrackName(File file) { - - // get file name without extension - String readableTrackName = file.getName(); - readableTrackName = readableTrackName.substring(0, readableTrackName.indexOf(".trackbook")); - - try { - // convert file name to date - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US); - Date trackDate = dateFormat.parse(readableTrackName); - - // convert date to track name string according to current locale - readableTrackName = DateFormat.getDateInstance(DateFormat.LONG, Locale.getDefault()).format(trackDate) + " - " + - DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()).format(trackDate); - - } catch (ParseException e) { - LogHelper.w(LOG_TAG, "Unable to parse file name into date object (yyyy-MM-dd-HH-mm-ss): " + e); - } - - return readableTrackName; - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/core/Tracklist.kt b/app/src/main/java/org/y20k/trackbook/core/Tracklist.kt new file mode 100644 index 0000000..8944ed8 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/core/Tracklist.kt @@ -0,0 +1,53 @@ +/* + * Tracklist.kt + * Implements the Tracklist data class + * A Tracklist stores a list of Tracks + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.core + +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.Expose +import kotlinx.android.parcel.Parcelize +import org.y20k.trackbook.Keys +import org.y20k.trackbook.helpers.TrackHelper +import java.util.* + + +/* + * Tracklist data class + */ +@Keep +@Parcelize +data class Tracklist (@Expose val tracklistFormatVersion: Int = Keys.CURRENT_TRACKLIST_FORMAT_VERSION, + @Expose val tracklistElements: MutableList = mutableListOf(), + @Expose var modificationDate: Date = Date()): Parcelable { + + /* Return trackelement for given track id */ + fun getTrackElement(trackId: Long): TracklistElement? { + tracklistElements.forEach { tracklistElement -> + if (TrackHelper.getTrackId(tracklistElement) == trackId) { + return tracklistElement + } + } + return null + } + + /* Create a deep copy */ + fun deepCopy(): Tracklist { + return Tracklist(tracklistFormatVersion, mutableListOf().apply { addAll(tracklistElements) }, modificationDate) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/core/TracklistElement.kt b/app/src/main/java/org/y20k/trackbook/core/TracklistElement.kt new file mode 100644 index 0000000..baa7e5a --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/core/TracklistElement.kt @@ -0,0 +1,46 @@ +/* + * TracklistElement.kt + * Implements the TracklistElement data class + * A TracklistElement data about a Track + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.core + +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.Expose +import kotlinx.android.parcel.Parcelize +import java.util.* + + +/* + * TracklistElement data class + */ +@Keep +@Parcelize +data class TracklistElement(@Expose var name: String, + @Expose val date: Date, + @Expose val dateString: String, + @Expose val durationString: String, + @Expose val length: Float, + @Expose val trackUriString: String, + @Expose val gpxUriString: String, + @Expose var starred: Boolean = false): Parcelable { + + /* Returns unique ID for TracklistElement - currently the start date */ + fun getTrackId(): Long { + return date.time + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/core/WayPoint.java b/app/src/main/java/org/y20k/trackbook/core/WayPoint.java deleted file mode 100755 index de1b86c..0000000 --- a/app/src/main/java/org/y20k/trackbook/core/WayPoint.java +++ /dev/null @@ -1,124 +0,0 @@ -/** - * WayPoint.java - * Implements the WayPoint class - * A WayPoint stores a location plus additional metadata - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.core; - -import android.location.Location; -import android.os.Bundle; -import android.os.Parcel; -import android.os.Parcelable; - - -/** - * WayPoint class - */ -public class WayPoint implements Parcelable { - - private Location mLocation; - private boolean mIsStopOver; - private float mDistanceToStartingPoint; - private final int mNumberSatellites; - - /* Constructor */ - public WayPoint(Location location, boolean isStopOver, float distanceToStartingPoint) { - mLocation = location; - mIsStopOver = isStopOver; - mDistanceToStartingPoint = distanceToStartingPoint; - - // save number of satellites - Bundle extras = location.getExtras(); - if (extras != null && extras.containsKey("satellites")) { - mNumberSatellites = extras.getInt("satellites", 0); - mLocation.setExtras(null); // necessary because Location Extras cause cannot be serialized properly by GSON - } else { - mNumberSatellites = 0; - } - - } - - /* Constructor used by CREATOR */ - protected WayPoint(Parcel in) { - mLocation = Location.CREATOR.createFromParcel(in); - mIsStopOver = in.readByte() != 0; - mDistanceToStartingPoint = in.readFloat(); - mNumberSatellites = in.readInt(); - } - - - /* CREATOR for WayPoint object used to do parcel related operations */ - public static final Creator CREATOR = new Creator() { - @Override - public WayPoint createFromParcel(Parcel in) { - return new WayPoint(in); - } - - @Override - public WayPoint[] newArray(int size) { - return new WayPoint[size]; - } - }; - - - /* Getter for mLocation */ - public Location getLocation() { - return mLocation; - } - - - /* Getter for mIsStopOver */ - public boolean getIsStopOver() { - return mIsStopOver; - } - - - /* Getter for mDistanceToStartingPoint */ - public float getDistanceToStartingPoint() { - return mDistanceToStartingPoint; - } - - - /* Setter for mLocation */ - public void setLocation(Location location) { - mLocation = location; - } - - - /* Setter for mIsStopOver */ - public void setIsStopOver(boolean isStopOver) { - mIsStopOver = isStopOver; - } - - - /* Setter for mDistanceToStartingPoint */ - public void setDistanceToStartingPoint(float distanceToStartingPoint) { - mDistanceToStartingPoint = distanceToStartingPoint; - } - - @Override - public int describeContents() { - return 0; - } - - - @Override - public void writeToParcel(Parcel parcel, int flags) { - mLocation.setExtras(null); // necessary because Location Extras cause cannot be serialized properly by GSON - mLocation.writeToParcel(parcel, flags); - parcel.writeByte((byte) (mIsStopOver ? 1 : 0)); - parcel.writeFloat(mDistanceToStartingPoint); - parcel.writeInt(mNumberSatellites); - } -} diff --git a/app/src/main/java/org/y20k/trackbook/core/WayPoint.kt b/app/src/main/java/org/y20k/trackbook/core/WayPoint.kt new file mode 100644 index 0000000..cbc1dd0 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/core/WayPoint.kt @@ -0,0 +1,54 @@ +/* + * WayPoint.kt + * Implements the WayPoint data class + * A WayPoint stores a location plus additional metadata + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.core + +import android.location.Location +import android.os.Parcelable +import androidx.annotation.Keep +import com.google.gson.annotations.Expose +import kotlinx.android.parcel.Parcelize + + +/* + * WayPoint data class + */ +@Keep +@Parcelize +data class WayPoint(@Expose val provider: String, + @Expose val latitude: Double, + @Expose val longitude: Double, + @Expose val altitude: Double, + @Expose val accuracy: Float, + @Expose val time: Long, + @Expose val distanceToStartingPoint: Float = 0f, + @Expose val numberSatellites: Int = 0, + @Expose var isStopOver: Boolean = false): Parcelable { + + + /* Converts WayPoint into Location */ + fun toLocation(): Location { + val location: Location = Location(provider) + location.latitude = latitude + location.longitude = longitude + location.altitude = altitude + location.accuracy = accuracy + location.time = time + return location + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/dialogs/ErrorDialog.kt b/app/src/main/java/org/y20k/trackbook/dialogs/ErrorDialog.kt new file mode 100644 index 0000000..48864c9 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/dialogs/ErrorDialog.kt @@ -0,0 +1,92 @@ +/* + * ErrorDialog.kt + * Implements the ErrorDialog object + * A ErrorDialog shows an error dialog with details + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.dialogs + +import android.content.Context +import android.content.DialogInterface +import android.text.method.ScrollingMovementMethod +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.y20k.trackbook.R +import org.y20k.trackbook.helpers.LogHelper + + +/* + * ErrorDialog object + */ +object ErrorDialog { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(ErrorDialog::class.java) + + + /* Construct and show dialog */ + fun show(context: Context, errorTitle: Int, errorMessage: Int, errorDetails: String = String()) { + // prepare dialog builder + val builder: MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(context, R.style.AlertDialogTheme) + + // set title + builder.setTitle(context.getString(errorTitle)) + + // get views + val inflater: LayoutInflater = LayoutInflater.from(context) + val view: View = inflater.inflate(R.layout.dialog_generic_with_details, null) + val errorMessageView: TextView = view.findViewById(R.id.dialog_message) as TextView + val errorDetailsLinkView: TextView = view.findViewById(R.id.dialog_details_link) as TextView + val errorDetailsView: TextView = view.findViewById(R.id.dialog_details) as TextView + + // set dialog view + builder.setView(view) + + // set detail view + if (errorDetails.isNotEmpty()) { + // show details link + errorDetailsLinkView.visibility = View.VISIBLE + + // allow scrolling on details view + errorDetailsView.movementMethod = ScrollingMovementMethod() + + // show and hide details on click + errorDetailsLinkView.setOnClickListener { + when (errorDetailsView.visibility) { + View.GONE -> errorDetailsView.visibility = View.VISIBLE + View.VISIBLE -> errorDetailsView.visibility = View.GONE + } + } + // set details text view + errorDetailsView.text = errorDetails + } else { + // hide details link + errorDetailsLinkView.visibility = View.GONE + } + + // set text views + errorMessageView.text = context.getString(errorMessage) + + // add okay button + builder.setPositiveButton(R.string.dialog_generic_button_okay, DialogInterface.OnClickListener { _, _ -> + // listen for click on okay button + // do nothing + }) + + // display error dialog + builder.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/dialogs/RenameTrackDialog.kt b/app/src/main/java/org/y20k/trackbook/dialogs/RenameTrackDialog.kt new file mode 100644 index 0000000..ebd8403 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/dialogs/RenameTrackDialog.kt @@ -0,0 +1,82 @@ +/* + * RenameTrackDialog.kt + * Implements the RenameTrackDialog class + * A RenameTrackDialog offers user to change name of track + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.dialogs + +import android.content.Context +import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.widget.EditText +import android.widget.TextView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.y20k.trackbook.R +import org.y20k.trackbook.helpers.LogHelper + + +/* + * RenameTrackDialog class + */ +class RenameTrackDialog (private var renameTrackListener: RenameTrackListener) { + + /* Interface used to communicate back to activity */ + interface RenameTrackListener { + fun onRenameTrackDialog(textInput: String) { + } + } + + /* Define log tag */ + private val TAG = LogHelper.makeLogTag(RenameTrackDialog::class.java.simpleName) + + + /* Construct and show dialog */ + fun show(context: Context, trackName: String) { + // prepare dialog builder + val builder: MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(context) + + // get input field + val inflater = LayoutInflater.from(context) + val view = inflater.inflate(R.layout.dialog_rename_track, null) + val inputField = view.findViewById(R.id.dialog_rename_track_input_edit_text) as EditText + + // pre-fill with current track name + inputField.setText(trackName, TextView.BufferType.EDITABLE) + inputField.setSelection(trackName.length) + inputField.inputType = InputType.TYPE_CLASS_TEXT + + // set dialog view + builder.setView(view) + + // add "add" button + builder.setPositiveButton(R.string.dialog_rename_track_button) { _, _ -> + // hand text over to initiating activity + inputField.text?.let { + renameTrackListener.onRenameTrackDialog(it.toString()) + } + } + + // add cancel button + builder.setNegativeButton(R.string.dialog_generic_button_cancel) { _, _ -> + // listen for click on cancel button + // do nothing + } + + // display add dialog + builder.show() + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/dialogs/YesNoDialog.kt b/app/src/main/java/org/y20k/trackbook/dialogs/YesNoDialog.kt new file mode 100644 index 0000000..83c391e --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/dialogs/YesNoDialog.kt @@ -0,0 +1,97 @@ +/* + * YesNoDialog + * Implements the YesNoDialog class + * A YesNoDialog asks the user if he/she wants to do something or notpackage org.y20k.trackbook.dialogs + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +import android.content.Context +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.y20k.trackbook.Keys +import org.y20k.trackbook.R +import org.y20k.trackbook.helpers.LogHelper + +/* + * YesNoDialog class + */ +class YesNoDialog (private var yesNoDialogListener: YesNoDialogListener) { + + /* Interface used to communicate back to activity */ + interface YesNoDialogListener { + fun onYesNoDialog(type: Int, dialogResult: Boolean, payload: Int, payloadString: String) { + } + } + + + /* Define log tag */ + private val TAG = LogHelper.makeLogTag(YesNoDialog::class.java.simpleName) + + + + + + /* Construct and show dialog - variant: message from string */ + fun show(context: Context, + type: Int, + title: Int = Keys.EMPTY_STRING_RESOURCE, + message: Int, + yesButton: Int = R.string.dialog_yes_no_positive_button_default, + noButton: Int = R.string.dialog_generic_button_cancel, + payload: Int = Keys.DIALOG_EMPTY_PAYLOAD_INT, + payloadString: String = Keys.DIALOG_EMPTY_PAYLOAD_STRING) { + // extract string from message resource and feed into main show method + show(context, type, title, context.getString(message), yesButton, noButton, payload, payloadString) + } + + + /* Construct and show dialog */ + fun show(context: Context, + type: Int, + title: Int = Keys.EMPTY_STRING_RESOURCE, + messageString: String, + yesButton: Int = R.string.dialog_yes_no_positive_button_default, + noButton: Int = R.string.dialog_generic_button_cancel, + payload: Int = Keys.DIALOG_EMPTY_PAYLOAD_INT, + payloadString: String = Keys.DIALOG_EMPTY_PAYLOAD_STRING) { + + // prepare dialog builder + val builder: MaterialAlertDialogBuilder = MaterialAlertDialogBuilder(context, R.style.AlertDialogTheme) + + // set title and message + builder.setMessage(messageString) + if (title != Keys.EMPTY_STRING_RESOURCE) { + builder.setTitle(context.getString(title)) + } + + + // add yes button + builder.setPositiveButton(yesButton) { _, _ -> + // listen for click on yes button + yesNoDialogListener.onYesNoDialog(type, true, payload, payloadString) + } + + // add no button + builder.setNegativeButton(noButton) { _, _ -> + // listen for click on no button + yesNoDialogListener.onYesNoDialog(type, false, payload, payloadString) + } + + // handle outside-click as "no" + builder.setOnCancelListener(){ + yesNoDialogListener.onYesNoDialog(type, false, payload, payloadString) + } + + // display dialog + builder.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/extensions/SharedPreferencesExt.kt b/app/src/main/java/org/y20k/trackbook/extensions/SharedPreferencesExt.kt new file mode 100644 index 0000000..b4836eb --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/extensions/SharedPreferencesExt.kt @@ -0,0 +1,29 @@ +/* + * SharedPreferencesExt.kt + * Implements the SharedPreferencesExt extension functions + * SharedPreferencesExt displays provides additional functions for dealing with shared preferences + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.extensions + + +import android.content.SharedPreferences + + +/* Puts a Double value in SharedPreferences */ +fun SharedPreferences.Editor.putDouble(key: String, double: Double) = putLong(key, java.lang.Double.doubleToRawLongBits(double)) + + +/* gets a Double value from SharedPreferences */ +fun SharedPreferences.getDouble(key: String, default: Double) = java.lang.Double.longBitsToDouble(getLong(key, java.lang.Double.doubleToRawLongBits(default))) \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/DateTimeHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/DateTimeHelper.kt new file mode 100644 index 0000000..c02d380 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/DateTimeHelper.kt @@ -0,0 +1,84 @@ +/* + * DateTimeHelper.kt + * Implements the DateTimeHelper object + * A DateTimeHelper provides helper methods for converting Date and Time objects + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.content.Context +import android.location.Location +import org.y20k.trackbook.Keys +import org.y20k.trackbook.R +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.TimeUnit + + +/* + * DateTimeHelper object + */ +object DateTimeHelper { + + /* Converts milliseconds to mm:ss or hh:mm:ss */ + fun convertToReadableTime(context: Context, milliseconds: Long): String { + var timeString: String = String() + val hours: Long = TimeUnit.MILLISECONDS.toHours(milliseconds) + val minutes: Long = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % TimeUnit.HOURS.toMinutes(1) + val seconds: Long = TimeUnit.MILLISECONDS.toSeconds(milliseconds) % TimeUnit.MINUTES.toSeconds(1) + val h: String = context.getString(R.string.abbreviation_hours) + val m: String = context.getString(R.string.abbreviation_minutes) + val s: String = context.getString(R.string.abbreviation_seconds) + + when (milliseconds >= Keys.ONE_HOUR_IN_MILLISECONDS) { + // CASE: format hh:mm:ss + true -> { + timeString = "$hours $h $minutes $m $seconds $s" + } + // CASE: format mm:ss + false -> { + timeString = "$minutes $m $seconds $s" + } + } + return timeString + } + + + /* Create sortable string from date - used for filenames */ + fun convertToSortableDateString(date: Date): String { + val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US) + return dateFormat.format(date) + } + + + /* Creates a readable string from date - used in the UI */ + fun convertToReadableDate(date: Date, dateStyle: Int = DateFormat.LONG): String { + return DateFormat.getDateInstance(dateStyle, Locale.getDefault()).format(date) + } + + + /* Calculates time difference between two locations */ + fun calculateTimeDistance(previousLocation: Location?, location: Location): Long { + var timeDifference: Long = 0L + // two data points needed to calculate time difference + if (previousLocation != null) { + // get time difference + timeDifference = location.time - previousLocation.time + } + return timeDifference + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/DialogHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/DialogHelper.java deleted file mode 100755 index 53e463b..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/DialogHelper.java +++ /dev/null @@ -1,87 +0,0 @@ -/** - * DialogHelper.java - * Implements the DialogHelper class - * A DialogHelper creates a customizable alert dialog - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; - - -/** - * DialogHelper class - */ -public class DialogHelper extends DialogFragment implements TrackbookKeys { - - /* Constructs a new instance */ - public static DialogHelper newInstance(int title, String message, int positiveButton, int negativeButton) { - DialogHelper fragment = new DialogHelper(); - Bundle args = new Bundle(); - args.putInt(ARG_DIALOG_TITLE, title); - args.putString(ARG_DIALOG_MESSAGE, message); - args.putInt(ARG_DIALOG_BUTTON_POSITIVE, positiveButton); - args.putInt(ARG_DIALOG_BUTTON_NEGATIVE, negativeButton); - fragment.setArguments(args); - return fragment; - } - - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - Bundle args = getArguments(); - - // get text elements - int title = args.getInt(ARG_DIALOG_TITLE); - String message = args.getString(ARG_DIALOG_MESSAGE); - int positiveButton = args.getInt(ARG_DIALOG_BUTTON_POSITIVE); - int negativeButton = args.getInt(ARG_DIALOG_BUTTON_NEGATIVE); - - // build dialog - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity()); - if (title != -1) { - dialogBuilder.setTitle(title); - } - dialogBuilder.setMessage(message); - dialogBuilder.setPositiveButton(positiveButton, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - Fragment target = getTargetFragment(); - if (target != null) { - target.onActivityResult(getTargetRequestCode(), Activity.RESULT_OK, getActivity().getIntent()); - } - } - } - ); dialogBuilder.setNegativeButton(negativeButton, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - Fragment target = getTargetFragment(); - if (target != null) { - target.onActivityResult(getTargetRequestCode(), Activity.RESULT_CANCELED, getActivity().getIntent()); - } - } - } - ); - - return dialogBuilder.create(); - } -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/DropdownAdapter.java b/app/src/main/java/org/y20k/trackbook/helpers/DropdownAdapter.java deleted file mode 100755 index edf2c44..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/DropdownAdapter.java +++ /dev/null @@ -1,187 +0,0 @@ -/** - * DropdownHelper.java - * Implements a dropdown menu - * The dropdown menu used to select tracks - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - - -package org.y20k.trackbook.helpers; - -import android.app.Activity; -import android.content.res.Resources; -import android.database.DataSetObserver; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.TextView; - -import org.y20k.trackbook.R; -import org.y20k.trackbook.core.TrackBundle; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; - -import androidx.annotation.Nullable; -import androidx.appcompat.widget.ThemedSpinnerAdapter; - - -/** - * DropdownHelper class - */ -public class DropdownAdapter extends BaseAdapter implements ThemedSpinnerAdapter, TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = DropdownAdapter.class.getSimpleName(); - - - /* Main class variables */ - private final Activity mActivity; - private final ThemedSpinnerAdapter.Helper mDropdownAdapterHelper; - private List mTrackBundleList; - - - /* Constructor */ - public DropdownAdapter(Activity activity) { - // store activity - mActivity = activity; - - // fill list with track bundles - initializeTrackBundleList(); - - // create an adapter helper - mDropdownAdapterHelper = new ThemedSpinnerAdapter.Helper(activity); - } - - - @Override - public void setDropDownViewTheme(@Nullable Resources.Theme theme) { - mDropdownAdapterHelper.setDropDownViewTheme(theme); - } - - - @Nullable - @Override - public Resources.Theme getDropDownViewTheme() { - return mDropdownAdapterHelper.getDropDownViewTheme(); - } - - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - // getView -> collapsed view of dropdown - View view = convertView; - if (view == null) { - LayoutInflater inflater = mDropdownAdapterHelper.getDropDownViewInflater(); - view = inflater.inflate(R.layout.custom_dropdown_item_collapsed, parent, false); - } - ((TextView) view).setText(getItem(position).getTrackName()); - return view; - } - - - @Override - public View getDropDownView(int position, View convertView, ViewGroup parent) { - // getDropDownView -> expanded view of dropdown - View view = convertView; - if (view == null) { - LayoutInflater inflater = mDropdownAdapterHelper.getDropDownViewInflater(); -// view = inflater.inflate(R.layout.custom_dropdown_item_expanded, parent, false); - view = inflater.inflate(R.layout.support_simple_spinner_dropdown_item, parent, false); - } - ((TextView) view).setText(getItem(position).getTrackName()); - return view; - } - - - @Override - public void registerDataSetObserver(DataSetObserver dataSetObserver) { - - } - - - @Override - public void unregisterDataSetObserver(DataSetObserver dataSetObserver) { - - } - - - @Override - public int getCount() { - return mTrackBundleList.size(); - } - - - @Override - public TrackBundle getItem(int i) { - return mTrackBundleList.get(i); - } - - - @Override - public long getItemId(int i) { - return 0; - } - - - @Override - public boolean hasStableIds() { - return false; - } - - - @Override - public int getItemViewType(int i) { - return 0; - } - - - @Override - public int getViewTypeCount() { - return 1; - } - - - @Override - public boolean isEmpty() { - return mTrackBundleList.size() == 0; - } - - - /* Refreshes the adapter data */ - public void refresh() { - // re-initialize the adapter's array list - initializeTrackBundleList(); - } - - - /* Initializes list of track bundles */ - private void initializeTrackBundleList() { - - // get list of files from storage - StorageHelper storageHelper = new StorageHelper(mActivity); - File files[] = storageHelper.getListOfTrackbookFiles(); - - // fill list with track bundles - mTrackBundleList = new ArrayList<>(); - for (File file : files) { - String fileName = file.getName(); - if (fileName.endsWith(FILE_TYPE_TRACKBOOK_EXTENSION) && !fileName.startsWith(FILE_NAME_TEMP)) { - mTrackBundleList.add(new TrackBundle(file)); - } - } - - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/ExportHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/ExportHelper.java deleted file mode 100755 index b39ce18..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/ExportHelper.java +++ /dev/null @@ -1,224 +0,0 @@ -/** - * ExportHelper.java - * Implements the ExportHelper class - * A ExportHelper can convert Track object into a GPX string - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import android.content.Context; -import android.content.Intent; -import android.location.Location; -import android.os.Environment; -import android.widget.Toast; - -import org.y20k.trackbook.R; -import org.y20k.trackbook.core.Track; -import org.y20k.trackbook.core.WayPoint; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; - -import androidx.core.content.FileProvider; - -/** - * ExportHelper class - */ -public final class ExportHelper extends FileProvider implements TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = ExportHelper.class.getSimpleName(); - - - /* Checks if a GPX file for given track is already present */ - public static boolean gpxFileExists(Track track) { - File folder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - return createFile(track, folder).exists(); - } - - - /* Exports given track to GPX */ - public static boolean exportToGpx(Context context, Track track) { - // get file for given track - File gpxFile = createFile(track, getDownloadFolder()); - - // get GPX string representation for given track - String gpxString = createGpxString(track); - - // write GPX file - if (writeGpxToFile(gpxString, gpxFile)) { - String toastMessage = context.getResources().getString(R.string.toast_message_export_success) + " " + gpxFile.toString(); - Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show(); - return true; - } else { - String toastMessage = context.getResources().getString(R.string.toast_message_export_fail) + " " + gpxFile.toString(); - Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show(); - return false; - } - } - - - /* Creates Intent used to bring up an Android share sheet */ - public static Intent getGpxFileIntent(Context context, Track track) { - - // create file in Cache directory for given track - File gpxFile = createFile(track, context.getCacheDir()); - - // get GPX string representation for given track - String gpxString = createGpxString(track); - - // write GPX file - if (writeGpxToFile(gpxString, gpxFile)) { - String toastMessage = context.getResources().getString(R.string.toast_message_export_success) + " " + gpxFile.toString(); - Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show(); - } else { - String toastMessage = context.getResources().getString(R.string.toast_message_export_fail) + " " + gpxFile.toString(); - Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show(); - } - - // create intent - String authority = "org.y20k.trackbook.exporthelper.provider"; - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_SEND); - intent.setDataAndType(FileProvider.getUriForFile(context, authority, gpxFile), "application/gpx+xml"); - intent.setType("application/gpx+xml"); - intent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(context, authority, gpxFile)); - - return intent; - } - - - /* Empties the internal chache directory */ - public static void emptyCacheDirectory(Context context) { - // todo implement a date check - delete only stuff that is a week old - File[] cacheFiles = context.getCacheDir().listFiles(); - for (File file: cacheFiles) { - file.delete(); - } - } - - - /* Get "Download" folder */ - private static File getDownloadFolder() { - File folder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - if (folder != null && !folder.exists()) { - LogHelper.v(LOG_TAG, "Creating new folder: " + folder.toString()); - folder.mkdirs(); - } - return folder; - } - - - /* Return a GPX filepath for a given track */ - private static File createFile(Track track, File folder) { - Date recordingStart = track.getRecordingStart(); - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US); - - return new File(folder, dateFormat.format(recordingStart) + FILE_TYPE_GPX_EXTENSION); - } - - - /* Writes given GPX string to given file */ - private static boolean writeGpxToFile (String gpxString, File gpxFile) { - // write track - try (BufferedWriter bw = new BufferedWriter(new FileWriter(gpxFile))) { - LogHelper.v(LOG_TAG, "Saving track to external storage: " + gpxFile.toString()); - bw.write(gpxString); - return true; - } catch (IOException e) { - LogHelper.e(LOG_TAG, "Unable to saving track to external storage (IOException): " + gpxFile.toString()); - return false; - } - } - - - /* Creates GPX formatted string */ - private static String createGpxString(Track track) { - String gpxString; - - // add header - gpxString = "\n" + - "\n"; - - // add track - gpxString = gpxString + addTrack(track); - - // add closing tag - gpxString = gpxString + "\n"; - - return gpxString; - } - - - /* Creates Track */ - private static String addTrack(Track track) { - StringBuilder gpxTrack = new StringBuilder(""); - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - - // add opening track tag - gpxTrack.append("\t\n"); - - // add name to track - gpxTrack.append("\t\t"); - gpxTrack.append("Trackbook Recording"); - gpxTrack.append("\n"); - - // add opening track segment tag - gpxTrack.append("\t\t\n"); - - // add route point - for (WayPoint wayPoint:track.getWayPoints()) { - // get location from waypoint - Location location = wayPoint.getLocation(); - - // add longitude and latitude - gpxTrack.append("\t\t\t\n"); - - // add time - gpxTrack.append("\t\t\t\t\n"); - - // add altitude - gpxTrack.append("\t\t\t\t"); - gpxTrack.append(location.getAltitude()); - gpxTrack.append("\n"); - - // add closing tag - gpxTrack.append("\t\t\t\n"); - } - - // add closing track segment tag - gpxTrack.append("\t\t\n"); - - // add closing track tag - gpxTrack.append("\t\n"); - - return gpxTrack.toString(); - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/FileHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/FileHelper.kt new file mode 100644 index 0000000..049e317 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/FileHelper.kt @@ -0,0 +1,431 @@ +/* + * FileHelper.kt + * Implements the FileHelper object + * A FileHelper provides helper methods for reading and writing files from and to device storage + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.content.Context +import android.database.Cursor +import android.graphics.Bitmap +import android.net.Uri +import android.provider.OpenableColumns +import androidx.core.net.toFile +import androidx.core.net.toUri +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import org.y20k.trackbook.Keys +import org.y20k.trackbook.core.Track +import org.y20k.trackbook.core.Tracklist +import org.y20k.trackbook.core.TracklistElement +import java.io.* +import java.text.NumberFormat +import java.util.* +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + + +/* + * FileHelper object + */ +object FileHelper { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(FileHelper::class.java) + + + /* Return an InputStream for given Uri */ + fun getTextFileStream(context: Context, uri: Uri): InputStream? { + var stream : InputStream? = null + try { + stream = context.contentResolver.openInputStream(uri) + } catch (e : Exception) { + e.printStackTrace() + } + return stream + } + + + /* Get file size for given Uri */ + fun getFileSize(context: Context, uri: Uri): Long { + val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null) + if (cursor != null) { + val sizeIndex: Int = cursor.getColumnIndex(OpenableColumns.SIZE) + cursor.moveToFirst() + val size: Long = cursor.getLong(sizeIndex) + cursor.close() + return size + } else { + return 0L + } + } + + + /* Get file name for given Uri */ + fun getFileName(context: Context, uri: Uri): String { + val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null) + if (cursor != null) { + val nameIndex: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor.moveToFirst() + val name: String = cursor.getString(nameIndex) + cursor.close() + return name + } else { + return String() + } + } + + + /* Clears given folder - keeps given number of files */ + fun clearFolder(folder: File?, keep: Int, deleteFolder: Boolean = false) { + if (folder != null && folder.exists()) { + val files = folder.listFiles() + val fileCount: Int = files.size + files.sortBy { it.lastModified() } + for (fileNumber in files.indices) { + if (fileNumber < fileCount - keep) { + files[fileNumber].delete() + } + } + if (deleteFolder && keep == 0) { + folder.delete() + } + } + } + + + /* Reads tracklist from storage using GSON */ + fun readTracklist(context: Context): Tracklist { + LogHelper.v(TAG, "Reading Tracklist - Thread: ${Thread.currentThread().name}") + // get JSON from text file + val json: String = readTextFile(context, getTracklistFileUri(context)) + var tracklist: Tracklist = Tracklist() + when (json.isNotBlank()) { + // convert JSON and return as tracklist + true -> try { + tracklist = getCustomGson().fromJson(json, Tracklist::class.java) + } catch (e: Exception) { + e.printStackTrace() + } + } + return tracklist + } + + + /* Reads track from storage using GSON */ + fun readTrack(context: Context, fileUri: Uri): Track { + // get JSON from text file + val json: String = readTextFile(context, fileUri) + var track: Track = Track() + when (json.isNotEmpty()) { + // convert JSON and return as track + true -> try { + track = getCustomGson().fromJson(json, Track::class.java) + } catch (e: Exception) { + e.printStackTrace() + } + } + return track + } + + + /* Deletes temp track file */ + fun deleteTempFile(context: Context) { + getTempFileUri(context).toFile().delete() + } + + + /* Checks if temp track file exists */ + fun tempFileExists(context: Context): Boolean { + return getTempFileUri(context).toFile().exists() + } + + + /* Creates Uri for Gpx file of a track */ + fun getGpxFileUri(context: Context, track: Track): Uri { + val fileName: String = DateTimeHelper.convertToSortableDateString(track.recordingStart) + Keys.GPX_FILE_EXTENSION + return File(context.getExternalFilesDir(Keys.FOLDER_GPX), fileName).toUri() + } + + + /* Creates Uri for json track file */ + fun getTrackFileUri(context: Context, track: Track): Uri { + val fileName: String = DateTimeHelper.convertToSortableDateString(track.recordingStart) + Keys.TRACKBOOK_FILE_EXTENSION + return File(context.getExternalFilesDir(Keys.FOLDER_TRACKS), fileName).toUri() + } + + + /* Creates Uri for json temp track file */ + fun getTempFileUri(context: Context): Uri { + return File(context.getExternalFilesDir(Keys.FOLDER_TEMP), Keys.TEMP_FILE).toUri() + } + + + /* Suspend function: Wrapper for saveTracklist */ + suspend fun addTrackAndSaveTracklistSuspended(context: Context, track: Track, modificationDate: Date = track.recordingStop) { + return suspendCoroutine { cont -> + val tracklist: Tracklist = readTracklist(context) + tracklist.tracklistElements.add(track.toTracklistElement(context)) + cont.resume(saveTracklist(context, tracklist, modificationDate)) + } + } + + + /* Suspend function: Wrapper for renameTrack */ + suspend fun renameTrackSuspended(context: Context, track: Track, newName: String) { + return suspendCoroutine { cont -> + cont.resume(renameTrack(context, track, newName)) + } + } + + + /* Suspend function: Wrapper for saveTracklist */ + suspend fun saveTracklistSuspended(context: Context, tracklist: Tracklist, modificationDate: Date) { + return suspendCoroutine { cont -> + cont.resume(saveTracklist(context, tracklist, modificationDate)) + } + } + + + /* Suspend function: Wrapper for saveTrack */ + suspend fun saveTrackSuspended(track: Track, saveGpxToo: Boolean) { + return suspendCoroutine { cont -> + cont.resume(saveTrack(track, saveGpxToo)) + } + } + + + /* Suspend function: Wrapper for saveTempTrack */ + suspend fun saveTempTrackSuspended(context: Context, track: Track) { + return suspendCoroutine { cont -> + cont.resume(saveTempTrack(context, track)) + } + } + + + /* Suspend function: Wrapper for deleteTrack */ + suspend fun deleteTrackSuspended(context: Context, position: Int, tracklist: Tracklist): Tracklist { + return suspendCoroutine { cont -> + cont.resume(deleteTrack(context, position, tracklist)) + } + } + + + /* Suspend function: Wrapper for readTracklist */ + suspend fun readTracklistSuspended(context: Context): Tracklist { + return suspendCoroutine {cont -> + cont.resume(readTracklist(context)) + } + } + + + /* Suspend function: Wrapper for copyFile */ + suspend fun saveCopyOfFileSuspended(context: Context, originalFileUri: Uri, targetFileUri: Uri) { + return suspendCoroutine { cont -> + cont.resume(copyFile(context, originalFileUri, targetFileUri, deleteOriginal = true)) + } + } + + + /* Save Track as JSON to storage */ + private fun saveTrack(track: Track, saveGpxToo: Boolean) { + val jsonString: String = getTrackJsonString(track) + if (jsonString.isNotBlank()) { + // write track file + writeTextFile(jsonString, Uri.parse(track.trackUriString)) + } + if (saveGpxToo) { + val gpxString: String = TrackHelper.createGpxString(track) + if (gpxString.isNotBlank()) { + // write GPX file + writeTextFile(gpxString, Uri.parse(track.gpxUriString)) + } + } + } + + + /* Save Temp Track as JSON to storage */ + private fun saveTempTrack(context: Context, track: Track) { + val json: String = getTrackJsonString(track) + if (json.isNotBlank()) { + writeTextFile(json, getTempFileUri(context)) + } + } + + + /* Saves track tracklist as JSON text file */ + private fun saveTracklist(context: Context, tracklist: Tracklist, modificationDate: Date) { + tracklist.modificationDate = modificationDate + // convert to JSON + val gson: Gson = getCustomGson() + var json: String = String() + try { + json = gson.toJson(tracklist) + } catch (e: Exception) { + e.printStackTrace() + } + if (json.isNotBlank()) { + // write text file + writeTextFile(json, getTracklistFileUri(context)) + } + } + + + /* Creates Uri for tracklist file */ + private fun getTracklistFileUri(context: Context): Uri { + return File(context.getExternalFilesDir(""), Keys.TRACKLIST_FILE).toUri() + } + + + + /* Renames track */ + private fun renameTrack(context: Context, track: Track, newName: String) { + // search track in tracklist + val tracklist: Tracklist = readTracklist(context) + var trackUriString: String = String() + tracklist.tracklistElements.forEach { tracklistElement -> + if (tracklistElement.getTrackId() == track.getTrackId()) { + // rename tracklist element + tracklistElement.name = newName + trackUriString = tracklistElement.trackUriString + } + } + if (trackUriString.isNotEmpty()) { + // save tracklist + saveTracklist(context, tracklist, GregorianCalendar.getInstance().time) + // rename track + track.name = newName + // save track + saveTrack(track, saveGpxToo = true) + } + } + + + /* Deletes track */ + private fun deleteTrack(context: Context, position: Int, tracklist: Tracklist): Tracklist { + val tracklistElement: TracklistElement = tracklist.tracklistElements[position] + // delete track files + tracklistElement.trackUriString.toUri().toFile().delete() + tracklistElement.gpxUriString.toUri().toFile().delete() + // remove track element from list + tracklist.tracklistElements.removeIf { TrackHelper.getTrackId(it) == TrackHelper.getTrackId(tracklistElement) } + saveTracklist(context, tracklist, GregorianCalendar.getInstance().time) + return tracklist + } + + + /* Copies file to specified target */ + private fun copyFile(context: Context, originalFileUri: Uri, targetFileUri: Uri, deleteOriginal: Boolean = false) { + val inputStream = context.contentResolver.openInputStream(originalFileUri) + val outputStream = context.contentResolver.openOutputStream(targetFileUri) + if (outputStream != null) { + inputStream?.copyTo(outputStream) + } + if (deleteOriginal) { + context.contentResolver.delete(originalFileUri, null, null) + } + } + + + /* Converts track to JSON */ + private fun getTrackJsonString(track: Track): String { + val gson: Gson = getCustomGson() + var json: String = String() + try { + json = gson.toJson(track) + } catch (e: Exception) { + e.printStackTrace() + } + return json + } + + + /* Creates a Gson object */ + private fun getCustomGson(): Gson { + val gsonBuilder = GsonBuilder() + gsonBuilder.setDateFormat("M/d/yy hh:mm a") + gsonBuilder.excludeFieldsWithoutExposeAnnotation() + return gsonBuilder.create() + } + + + + /* Converts byte value into a human readable format */ + // Source: https://programming.guide/java/formatting-byte-size-to-human-readable-format.html + fun getReadableByteCount(bytes: Long, si: Boolean = true): String { + + // check if Decimal prefix symbol (SI) or Binary prefix symbol (IEC) requested + val unit: Long = if (si) 1000L else 1024L + + // just return bytes if file size is smaller than requested unit + if (bytes < unit) return "$bytes B" + + // calculate exp + val exp: Int = (Math.log(bytes.toDouble()) / Math.log(unit.toDouble())).toInt() + + // determine prefix symbol + val prefix: String = ((if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i") + + // calculate result and set number format + val result: Double = bytes / Math.pow(unit.toDouble(), exp.toDouble()) + val numberFormat = NumberFormat.getNumberInstance() + numberFormat.maximumFractionDigits = 1 + + return numberFormat.format(result) + " " + prefix + "B" + } + + + /* Reads InputStream from file uri and returns it as String */ + private fun readTextFile(context: Context, fileUri: Uri): String { + // todo read https://commonsware.com/blog/2016/03/15/how-consume-content-uri.html + // https://developer.android.com/training/secure-file-sharing/retrieve-info + val file: File = fileUri.toFile() + // check if file exists + if (!file.exists()) { + return String() + } + // read until last line reached + val stream: InputStream = file.inputStream() + val reader: BufferedReader = BufferedReader(InputStreamReader(stream)) + val builder: StringBuilder = StringBuilder() + reader.forEachLine { + builder.append(it) + builder.append("\n") } + stream.close() + return builder.toString() + } + + + /* Writes given text to file on storage */ + private fun writeTextFile(text: String, fileUri: Uri) { + val file: File = fileUri.toFile() + file.writeText(text) + } + + + /* Writes given bitmap as image file to storage */ + private fun writeImageFile(context: Context, bitmap: Bitmap, file: File, format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, quality: Int = 75) { + if (file.exists()) file.delete () + try { + val out = FileOutputStream(file) + bitmap.compress(format, quality, out) + out.flush() + out.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/ImportHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/ImportHelper.kt new file mode 100644 index 0000000..6b42d79 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/ImportHelper.kt @@ -0,0 +1,165 @@ +/* + * ImportHelper.kt + * Implements the ImportHelper object + * A ImportHelper manages the one-time import of old .trackbook files + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.content.Context +import android.location.Location +import com.google.gson.GsonBuilder +import com.google.gson.annotations.SerializedName +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.y20k.trackbook.Keys +import org.y20k.trackbook.core.Track +import org.y20k.trackbook.core.WayPoint +import java.io.BufferedReader +import java.io.File +import java.io.InputStream +import java.io.InputStreamReader +import java.util.* + + +/* + * ImportHelper data class + */ +object ImportHelper { + + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(ImportHelper::class.java) + + + + /* Converts older tracks of type .trackbook into the new format */ + fun convertOldTracks(context: Context) { + val oldTracks: ArrayList = arrayListOf() + val trackFolder: File? = context.getExternalFilesDir(Keys.FOLDER_TRACKS) + + if (trackFolder != null && trackFolder.exists() && trackFolder.isDirectory) { + trackFolder.listFiles()?.forEach { file -> + if (file.name.endsWith(".trackbook")) { + // read until last line reached + val stream: InputStream = file.inputStream() + val reader: BufferedReader = BufferedReader(InputStreamReader(stream)) + val builder: StringBuilder = StringBuilder() + reader.forEachLine { + builder.append(it) + builder.append("\n") } + stream.close() + // get content of file + val fileContent: String = builder.toString() + // get LegacyTrack from JSON + val gsonBuilder = GsonBuilder() + gsonBuilder.setDateFormat("M/d/yy hh:mm a") + val oldTrack: LegacyTrack = gsonBuilder.create().fromJson(fileContent, LegacyTrack::class.java) + oldTracks.add(oldTrack.toTrack()) + } + } + } + + // save track using "deferred await" + if (oldTracks.isNotEmpty()) { + GlobalScope.launch { + oldTracks.forEach { oldTrack -> + // step 1: create and store filenames for json and gpx files + oldTrack.trackUriString = FileHelper.getTrackFileUri(context, oldTrack).toString() + oldTrack.gpxUriString = FileHelper.getGpxFileUri(context, oldTrack).toString() + // step 2: save track + FileHelper.saveTrackSuspended(oldTrack, saveGpxToo = true) + // step 3: save tracklist + FileHelper.addTrackAndSaveTracklistSuspended(context, oldTrack) + } + } + } + + } + + + /* + * Inner class: Legacy version of Track - used for one-time import only + * Warning: Works only as long as targetSdkVersion < 28 + */ + private data class LegacyTrack ( + @SerializedName("b") var mTrackFormatVersion: Int = 0, + @SerializedName("c") var mWayPoints: List, + @SerializedName("d") var mTrackLength: Float = 0f, + @SerializedName("e") var mDuration: Long = 0, + @SerializedName("f") var mStepCount: Float = 0f, + @SerializedName("g") var mRecordingStart: Date = GregorianCalendar.getInstance().time, + @SerializedName("h") var mRecordingStop: Date = mRecordingStart, + @SerializedName("i") var mMaxAltitude: Double = 0.0, + @SerializedName("j") var mMinAltitude: Double = 0.0, + @SerializedName("k") var mPositiveElevation: Double = 0.0, + @SerializedName("l") var mNegativeElevation: Double = 0.0) { + + + /* Converts */ + fun toTrack():Track { + val track: Track = Track() + track.trackFormatVersion = mTrackFormatVersion + mWayPoints.forEach { legacyWayPoint -> + val wayPoint: WayPoint= WayPoint( + provider = legacyWayPoint.mLocation.provider, + latitude = legacyWayPoint.mLocation.latitude, + longitude = legacyWayPoint.mLocation.longitude, + altitude = legacyWayPoint.mLocation.altitude, + accuracy = legacyWayPoint.mLocation.accuracy, + time = legacyWayPoint.mLocation.time, + distanceToStartingPoint = legacyWayPoint.mDistanceToStartingPoint, + numberSatellites = legacyWayPoint.mNumberSatellites, + isStopOver = legacyWayPoint.mIsStopOver + ) + track.wayPoints.add(wayPoint) + } + track.length = mTrackLength + track.duration = mDuration + track.stepCount = mStepCount + track.recordingStart = mRecordingStart + track.recordingStop = mRecordingStop + track.maxAltitude = mMaxAltitude + track.minAltitude = mMinAltitude + track.positiveElevation = mPositiveElevation + track.negativeElevation = mNegativeElevation + track.latitude = track.wayPoints[0].latitude + track.longitude = track.wayPoints[0].longitude + track.name = DateTimeHelper.convertToReadableDate(mRecordingStart) + return track + } + + } + /* + * End of inner class + */ + + + /* + * Inner class: Legacy version of WayPoint - used for one-time import only + * Warning: Works only as long as targetSdkVersion < 28 + */ + private data class LegacyWayPoint ( + var mLocation: Location, + var mIsStopOver: Boolean = false, + var mDistanceToStartingPoint: Float = 0f, + var mNumberSatellites: Int = 0) { + } + /* + * End of inner class + */ + + + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.java deleted file mode 100755 index c6295d3..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.java +++ /dev/null @@ -1,96 +0,0 @@ -/** - * LengthUnitHelper.java - * Implements the LengthUnitHelper class - * A LengthUnitHelper offers helper methods for dealing with unit systems and locales - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import java.text.NumberFormat; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - - -/** - * LengthUnitHelper class - */ -public final class LengthUnitHelper implements TrackbookKeys { - - - /* Converts for the default locale a distance value to a readable string */ - public static String convertDistanceToString(double distance) { - return convertDistanceToString(distance, getUnitSystem()); - } - - - /* Converts for the given uni System a distance value to a readable string */ - public static String convertDistanceToString(double distance, int unitSystem) { - String unit; - NumberFormat numberFormat = NumberFormat.getNumberInstance(); - - // check for locale and set unit system accordingly - if (unitSystem == IMPERIAL) { - // miles and feet - if (distance > 1610) { - // convert distance to miles - distance = distance * 0.000621371192; - // set measurement unit - unit = "mi"; - // set number precision - numberFormat.setMaximumFractionDigits(2); - } else { - // convert distance to feet - distance = distance * 3.28084f; - // set measurement unit - unit = "ft"; - // set number precision - numberFormat.setMaximumFractionDigits(0); - } - - - } else { - // kilometer and meter - if (distance >= 1000) { - // convert distance to kilometer - distance = distance * 0.001f; - // set measurement unit - unit = "km"; - // set number precision - numberFormat.setMaximumFractionDigits(2); - } else { - // set measurement unit - unit = "m"; - // set number precision - numberFormat.setMaximumFractionDigits(0); - } - - } - // format distance according to current locale - return numberFormat.format(distance) + " " + unit; - } - - - /* Determines which unit system the device is using (metric or imperial) */ - public static int getUnitSystem() { - // America (US), Liberia (LR), Myanmar(MM) use the imperial system - List imperialSystemCountries = Arrays.asList("US", "LR", "MM"); - String countryCode = Locale.getDefault().getCountry(); - if (imperialSystemCountries.contains(countryCode)){ - return IMPERIAL; - } else { - return METRIC; - } - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.kt new file mode 100644 index 0000000..96f8106 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/LengthUnitHelper.kt @@ -0,0 +1,95 @@ +/* + * LengthUnitHelper.kt + * Implements the LengthUnitHelper object + * A LengthUnitHelper offers helper methods for dealing with unit systems and locales + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import java.text.NumberFormat +import java.util.* + + +/* + * LengthUnitHelper object + */ +object LengthUnitHelper { + + + /* Converts for the given uni System a distance value to a readable string */ + fun convertDistanceToString(distance: Float, useImperial: Boolean = false): String { + return convertDistanceToString(distance.toDouble(), useImperial) + } + + + /* Converts for the given uni System a distance value to a readable string */ + fun convertDistanceToString(distance: Double, useImperial: Boolean = false): String { + val readableDistance: Double + val unit: String + val numberFormat = NumberFormat.getNumberInstance() + + // check for locale and set unit system accordingly + when (useImperial) { + // CASE: miles and feet + true -> { + if (distance > 1610) { + // convert distance to miles + readableDistance = distance * 0.000621371192f + // set measurement unit + unit = "mi" + // set number precision + numberFormat.maximumFractionDigits = 2 + } else { + // convert distance to feet + readableDistance = distance * 3.28084f + // set measurement unit + unit = "ft" + // set number precision + numberFormat.maximumFractionDigits = 0 + } + } + // CASE: kilometer and meter + false -> { + if (distance >= 1000) { + // convert distance to kilometer + readableDistance = distance * 0.001f + // set measurement unit + unit = "km" + // set number precision + numberFormat.maximumFractionDigits = 2 + } else { + // no need to convert + readableDistance = distance + // set measurement unit + unit = "m" + // set number precision + numberFormat.maximumFractionDigits = 0 + } + } + } + + // format distance according to current locale + return "${numberFormat.format(readableDistance)} $unit" + } + + + /* Determines which unit system the device is using (metric or imperial) */ + fun useImperialUnits(): Boolean { + // America (US), Liberia (LR), Myanmar(MM) use the imperial system + val imperialSystemCountries = Arrays.asList("US", "LR", "MM") + val countryCode = Locale.getDefault().country + return imperialSystemCountries.contains(countryCode) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.java deleted file mode 100755 index 41a4d6f..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.java +++ /dev/null @@ -1,302 +0,0 @@ -/** - * LocationHelper.java - * Implements the LocationHelper class - * A LocationHelper offers helper methods for dealing with location issues - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import android.content.Context; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.os.SystemClock; -import android.provider.Settings; - -import java.util.List; -import java.util.Locale; -import java.util.concurrent.TimeUnit; - -import androidx.annotation.Nullable; - - -/** - * LocationHelper class - */ -public final class LocationHelper implements TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = LocationHelper.class.getSimpleName(); - - - /* Determines last known location */ - public static Location determineLastKnownLocation(LocationManager locationManager) { - // define variables - List locationProviders = locationManager.getProviders(true); - Location gpsLocation = null; - Location networkLocation = null; - - // set location providers - String gpsProvider = LocationManager.GPS_PROVIDER; - String networkProvider = LocationManager.NETWORK_PROVIDER; - - - if (locationProviders.contains(gpsProvider)) { - // get last know location from gps - try { - gpsLocation = locationManager.getLastKnownLocation(gpsProvider); - } catch (SecurityException e) { - // catches permission problems - e.printStackTrace(); - } - } - - if (locationProviders.contains(networkProvider)) { - // get last known location from wifi and cell - try { - networkLocation = locationManager.getLastKnownLocation(networkProvider); - } catch (SecurityException e) { - // catches permission problems - e.printStackTrace(); - } - } - - if (gpsLocation == null) { - return networkLocation; - } else if (networkLocation == null) { - return gpsLocation; - } else if (isBetterLocation(gpsLocation, networkLocation)) { - return gpsLocation; - } else { - return networkLocation; - } - } - - - /* Determines whether one location reading is better than the current location fix */ - public static boolean isBetterLocation(Location location, Location currentBestLocation) { - // credit: the isBetterLocation method was sample code from: https://developer.android.com/guide/topics/location/strategies.html - - if (currentBestLocation == null) { - // a new location is always better than no location - return true; - } - - // check whether the new location fix is newer or older - long timeDelta = location.getElapsedRealtimeNanos() - currentBestLocation.getElapsedRealtimeNanos(); - boolean isSignificantlyNewer = timeDelta > ONE_MINUTE_IN_NANOSECONDS; - boolean isSignificantlyOlder = timeDelta < -ONE_MINUTE_IN_NANOSECONDS; - boolean isNewer = timeDelta > 0; - - // if it's been more than two minutes since the current location, use the new location because the user has likely moved - if (isSignificantlyNewer) { - return true; - } else if (isSignificantlyOlder) { - return false; - } - - // check whether the new location fix is more or less accurate - int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy()); - boolean isLessAccurate = accuracyDelta > 0; - boolean isMoreAccurate = accuracyDelta < 0; - boolean isSignificantlyLessAccurate = accuracyDelta > 200; - - // check if the old and new location are from the same provider - boolean isFromSameProvider = isSameProvider(location.getProvider(), currentBestLocation.getProvider()); - - // determine location quality using a combination of timeliness and accuracy - if (isMoreAccurate) { - return true; - } else if (isNewer && !isLessAccurate) { - return true; - } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) { - return true; - } - return false; - } - - - /* Checks accuracy of given location */ - public static boolean isAccurate(Location location) { - return location.getAccuracy() < FIFTY_METER_RADIUS; - } - - - /* Checks if given location is newer than two minutes */ - public static boolean isCurrent(Location location) { - if (location == null) { - return false; - } else { - long locationAge = SystemClock.elapsedRealtimeNanos() - location.getElapsedRealtimeNanos(); - return locationAge < ONE_MINUTE_IN_NANOSECONDS; - } - } - - - /* Checks if given location is a new WayPoint */ - public static boolean isNewWayPoint(Location lastLocation, Location newLocation, float averageSpeed) { - float distance = newLocation.distanceTo(lastLocation); - long timeDifference = newLocation.getElapsedRealtimeNanos() - lastLocation.getElapsedRealtimeNanos(); - - if (newLocation.getProvider().equals(LocationManager.NETWORK_PROVIDER)) { - // calculate speed difference - float speedDifference; - float currentSpeed = distance / ((float)timeDifference / ONE_SECOND_IN_NANOSECOND); - if (currentSpeed > averageSpeed) { - speedDifference = currentSpeed / averageSpeed; - } else { - speedDifference = averageSpeed / currentSpeed; - } - - // SPECIAL CASE network: plausibility check for network provider. looking for sudden location jump errors - if (averageSpeed != 0f && currentSpeed > 10f && speedDifference > 2f) { - // implausible location (speed is high (10 m/s == 36km/h) and has doubled) - return false; - } - - // SPECIAL CASE network: if last location came from gps. only accept location fixes with decent accuracy - if (lastLocation.getProvider().equals(LocationManager.GPS_PROVIDER) && newLocation.getAccuracy() < 66) { - // network locations tend to be too in accurate - return false; - } - - // DEFAULT network: distance is bigger than 30 meters and time difference bigger than 12 seconds - return distance > 30 && timeDifference >= 12 * ONE_SECOND_IN_NANOSECOND; // TODO add minimal accuracy - - } else { - // DEFAULT GPS: distance is bigger than 10 meters and time difference bigger than 12 seconds - return distance > 10 && timeDifference >= 12 * ONE_SECOND_IN_NANOSECOND; - } - - } - - - /* Checks if given location is a stop over */ - public static boolean isStopOver(@Nullable Location previousLocation, Location newLocation) { - if (previousLocation != null) { - long timeDifference = newLocation.getElapsedRealtimeNanos() - previousLocation.getElapsedRealtimeNanos(); - return timeDifference >= FIVE_MINUTES_IN_NANOSECONDS; - } else { - return false; - } - } - - - /* Registers gps and network location listeners */ - public static void registerLocationListeners(LocationManager locationManager, LocationListener gpsListener, LocationListener networkListener) { - LogHelper.v(LOG_TAG, "Registering location listeners."); - - // get location providers - List locationProviders = locationManager.getAllProviders(); - - // got GPS location provider? - if (gpsListener != null && locationProviders.contains(LocationManager.GPS_PROVIDER)) { - try { - // register GPS location listener and request updates - locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, gpsListener); - LogHelper.v(LOG_TAG, "Registering gps listener."); - } catch (SecurityException e) { - // catches permission problems - e.printStackTrace(); - } - } - - // got network location provider? - if (networkListener != null && locationProviders.contains(LocationManager.NETWORK_PROVIDER)) { - try { - // register network location listener and request updates - locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, networkListener); - LogHelper.v(LOG_TAG, "Registering network listener."); - } catch (SecurityException e) { - // catches permission problems - e.printStackTrace(); - } - } - } - - - /* Removes gps and network location listeners */ - public static void removeLocationListeners(LocationManager locationManager, LocationListener gpsListener, LocationListener networkListener) { - LogHelper.v(LOG_TAG, "Removing location listeners."); - - // get location providers - List locationProviders = locationManager.getAllProviders(); - - // got GPS location provider? - if (locationProviders.contains(LocationManager.GPS_PROVIDER) && gpsListener != null) { - try { - // remove GPS listener - locationManager.removeUpdates(gpsListener); - LogHelper.v(LOG_TAG, "Removing gps listener."); - } catch (SecurityException e) { - // catches permission problems - e.printStackTrace(); - } - } - - // got network location provider? - if (locationProviders.contains(LocationManager.NETWORK_PROVIDER) && networkListener != null) { - try { - // remove network listener - locationManager.removeUpdates(networkListener); - LogHelper.v(LOG_TAG, "Removing network listener."); - } catch (SecurityException e) { - // catches permission problems - e.printStackTrace(); - } - } - - } - - - /* Converts milliseconds to mm:ss or hh:mm:ss */ - public static String convertToReadableTime(long milliseconds, boolean includeHours) { - - if (includeHours) { - // format hh:mm:ss - return String.format(Locale.ENGLISH, "%02d:%02d:%02d", TimeUnit.MILLISECONDS.toHours(milliseconds), - TimeUnit.MILLISECONDS.toMinutes(milliseconds) % TimeUnit.HOURS.toMinutes(1), - TimeUnit.MILLISECONDS.toSeconds(milliseconds) % TimeUnit.MINUTES.toSeconds(1)); - } else if (TimeUnit.MILLISECONDS.toHours(milliseconds) < 1) { - // format mm:ss - return String.format(Locale.ENGLISH, "%02d:%02d", TimeUnit.MILLISECONDS.toMinutes(milliseconds) % TimeUnit.HOURS.toMinutes(1), - TimeUnit.MILLISECONDS.toSeconds(milliseconds) % TimeUnit.MINUTES.toSeconds(1)); - } else { - return null; - } - - } - - - /* Check if any location provider is enabled */ - public static boolean checkLocationSystemSetting(Context context) { - int locationSettingState = 0; - try { - locationSettingState = Settings.Secure.getInt(context.getContentResolver(), Settings.Secure.LOCATION_MODE); - } catch (Settings.SettingNotFoundException e) { - e.printStackTrace(); - } - return locationSettingState != Settings.Secure.LOCATION_MODE_OFF; - } - - - /* Checks whether two location providers are the same */ - private static boolean isSameProvider(String provider1, String provider2) { - // credit: the isSameProvider method was sample code from: https://developer.android.com/guide/topics/location/strategies.html - if (provider1 == null) { - return provider2 == null; - } - return provider1.equals(provider2); - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt new file mode 100644 index 0000000..888369e --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/LocationHelper.kt @@ -0,0 +1,218 @@ +/* + * LocationHelper.kt + * Implements the LocationHelper object + * A LocationHelper offers helper methods for dealing with location issues + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationManager +import android.os.SystemClock +import androidx.core.content.ContextCompat +import org.y20k.trackbook.Keys +import org.y20k.trackbook.core.Track +import java.util.* + + +/* + * Keys object + */ +object LocationHelper { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(LocationHelper::class.java) + + + /* Get default location */ + fun getDefaultLocation(): Location { + val defaultLocation: Location = Location(LocationManager.NETWORK_PROVIDER) + defaultLocation.latitude = Keys.DEFAULT_LATITUDE + defaultLocation.longitude = Keys.DEFAULT_LONGITUDE + defaultLocation.accuracy = Keys.DEFAULT_ACCURACY + defaultLocation.altitude = Keys.DEFAULT_ALTITUDE + defaultLocation.time = Keys.DEFAULT_DATE.time + return defaultLocation + } + + + /* Checks if a location is older than one minute */ + fun isOldLocation(location: Location): Boolean { + // check how many milliseconds the given location is old + return GregorianCalendar.getInstance().time.time - location.time > Keys.SIGNIFICANT_TIME_DIFFERENCE + } + + + /* Tries to return the last location that the system has stored */ + fun getLastKnownLocation(context: Context): Location { + // get last location that Trackbook has stored + var lastKnownLocation: Location = PreferencesHelper.loadCurrentBestLocation(context) + // try to get the last location the system has stored - it is probably more recent + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + val lastKnownLocationGps: Location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) ?: lastKnownLocation + val lastKnownLocationNetwork: Location = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) ?: lastKnownLocation + when (isBetterLocation(lastKnownLocationGps, lastKnownLocationNetwork)) { + true -> lastKnownLocation = lastKnownLocationGps + false -> lastKnownLocation = lastKnownLocationNetwork + } + } + return lastKnownLocation + } + + + /* Determines whether one Location reading is better than the current Location fix */ + fun isBetterLocation(location: Location, currentBestLocation: Location?): Boolean { + // Credit: https://developer.android.com/guide/topics/location/strategies.html#BestEstimate + + if (currentBestLocation == null) { + // a new location is always better than no location + return true + } + + // check whether the new location fix is newer or older + val timeDelta: Long = location.time - currentBestLocation.time + val isSignificantlyNewer: Boolean = timeDelta > Keys.SIGNIFICANT_TIME_DIFFERENCE + val isSignificantlyOlder:Boolean = timeDelta < -Keys.SIGNIFICANT_TIME_DIFFERENCE + + when { + // if it's been more than two minutes since the current location, use the new location because the user has likely moved + isSignificantlyNewer -> return true + // if the new location is more than two minutes older, it must be worse + isSignificantlyOlder -> return false + } + + // check whether the new location fix is more or less accurate + val isNewer: Boolean = timeDelta > 0L + val accuracyDelta: Float = location.accuracy - currentBestLocation.accuracy + val isLessAccurate: Boolean = accuracyDelta > 0f + val isMoreAccurate: Boolean = accuracyDelta < 0f + val isSignificantlyLessAccurate: Boolean = accuracyDelta > 200f + + // check if the old and new location are from the same provider + val isFromSameProvider: Boolean = location.provider == currentBestLocation.provider + + // determine location quality using a combination of timeliness and accuracy + return when { + isMoreAccurate -> true + isNewer && !isLessAccurate -> true + isNewer && !isSignificantlyLessAccurate && isFromSameProvider -> true + else -> false + } + } + + + /* Checks if GPS location provider is available and enabled */ + fun isGpsEnabled(locationManager: LocationManager): Boolean { + if (locationManager.allProviders.contains(LocationManager.GPS_PROVIDER)) { + return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) + } else { + return false + } + } + + + /* Checks if Network location provider is available and enabled */ + fun isNetworkEnabled(locationManager: LocationManager): Boolean { + if (locationManager.allProviders.contains(LocationManager.NETWORK_PROVIDER)) { + return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } else { + return false + } + } + + + + /* Checks if given location is new */ + fun isRecentEnough(location: Location): Boolean { + val locationAge: Long = SystemClock.elapsedRealtimeNanos() - location.elapsedRealtimeNanos + return locationAge < Keys.DEFAULT_THRESHOLD_LOCATION_AGE + } + + + /* Checks if given location is accurate */ + fun isAccurateEnough(location: Location, locationAccuracyThreshold: Int): Boolean { + val isAccurate: Boolean + when (location.provider) { + LocationManager.GPS_PROVIDER -> isAccurate = location.accuracy < locationAccuracyThreshold + else -> isAccurate = location.accuracy < locationAccuracyThreshold + 10 // a bit more relaxed when location comes from network provider + } + return isAccurate + } + + + /* Checks if given location is different enough compared to previous location */ + fun isDifferentEnough(previousLocation: Location?, location: Location): Boolean { + // check if previous location is (not) available + if (previousLocation == null) return true + // check if distance between is large enough + val distanceThreshold: Float + val averageAccuracy: Float = (previousLocation.accuracy + location.accuracy) / 2 + // increase the distance threshold if one or both locations are + if (averageAccuracy > Keys.DEFAULT_THRESHOLD_DISTANCE) { + distanceThreshold = averageAccuracy + } else { + distanceThreshold = Keys.DEFAULT_THRESHOLD_DISTANCE + } + LogHelper.e(TAG, "distanceThreshold -> $distanceThreshold") // todo remove + // location is different when far enough away from previous location + return calculateDistance(previousLocation, location) > distanceThreshold + } + + + /* Calculates distance between two locations */ + fun calculateDistance(previousLocation: Location?, location: Location): Float { + var distance: Float = 0f + // two data points needed to calculate distance + if (previousLocation != null) { + // add up distance + distance = previousLocation.distanceTo(location) + } + return distance + } + + + /* Calculate elevation differences */ + fun calculateElevationDifferences(previousLocation: Location?, location: Location, track: Track): Pair { + // store current values + var positiveElevation: Double = track.positiveElevation + var negativeElevation: Double = track.negativeElevation + if (previousLocation != null) { + // factor is bigger than 1 if the time stamp difference is larger than the movement recording interval + val timeDifferenceFactor: Long = (location.time - previousLocation.time) / Keys.ADD_WAYPOINT_TO_TRACK_INTERVAL + // get elevation difference and sum it up + val altitudeDifference: Double = location.altitude - previousLocation.altitude + if (altitudeDifference > 0 && altitudeDifference < Keys.ALTITUDE_MEASUREMENT_ERROR_THRESHOLD * timeDifferenceFactor && location.altitude != Keys.DEFAULT_ALTITUDE) { + positiveElevation = track.positiveElevation + altitudeDifference // upwards movement + } + if (altitudeDifference < 0 && altitudeDifference > -Keys.ALTITUDE_MEASUREMENT_ERROR_THRESHOLD * timeDifferenceFactor && location.altitude != Keys.DEFAULT_ALTITUDE) { + negativeElevation = track.negativeElevation + altitudeDifference // downwards movement + } + } + return Pair(positiveElevation, negativeElevation) + } + + + /* Checks if given location is a stop over */ + fun isStopOver(previousLocation: Location?, location: Location): Boolean { + if (previousLocation == null) return false + // check how many milliseconds the given locations are apart + return location.time - previousLocation.time > Keys.STOP_OVER_THRESHOLD + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/LogHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/LogHelper.java deleted file mode 100755 index eae3ae4..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/LogHelper.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * LogHelper.java - * Implements the LogHelper class - * A LogHelper wraps the logging calls to be able to strip them out of release versions - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - - -package org.y20k.trackbook.helpers; - -import android.util.Log; - -import org.y20k.trackbook.BuildConfig; - - -/** - * LogHelper class - */ -public final class LogHelper { - - private final static boolean mTesting = false; - - public static void d(final String tag, String message) { - // include logging only in debug versions - if (BuildConfig.DEBUG || mTesting) { - Log.d(tag, message); - } - } - - - public static void v(final String tag, String message) { - // include logging only in debug versions - if (BuildConfig.DEBUG || mTesting) { - Log.v(tag, message); - } - } - - - public static void e(final String tag, String message) { - Log.e(tag, message); - } - - - public static void i(final String tag, String message) { - Log.i(tag, message); - } - - - public static void w(final String tag, String message) { - Log.w(tag, message); - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/LogHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/LogHelper.kt new file mode 100644 index 0000000..5ca9427 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/LogHelper.kt @@ -0,0 +1,115 @@ +/* + * LogHelper.kt + * Implements the LogHelper object + * A LogHelper wraps the logging calls to be able to strip them out of release versions + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.util.Log +import org.y20k.trackbook.BuildConfig + + +/* + * LogHelper object + */ +object LogHelper { + + private const val TESTING: Boolean = true // set to "false" + private const val LOG_PREFIX: String = "trackbook_" + private const val MAX_LOG_TAG_LENGTH: Int = 64 + private const val LOG_PREFIX_LENGTH: Int = LOG_PREFIX.length + + fun makeLogTag(str: String): String { + return if (str.length > MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH) { + LOG_PREFIX + str.substring(0, MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH - 1) + } else LOG_PREFIX + str + } + + fun makeLogTag(cls: Class<*>): String { + // don't use this when obfuscating class names + return makeLogTag(cls.simpleName) + } + + fun v(tag: String, vararg messages: Any) { + // Only log VERBOSE if build type is DEBUG or if TESTING is true + if (BuildConfig.DEBUG || TESTING) { + log(tag, Log.VERBOSE, null, *messages) + } + } + + fun d(tag: String, vararg messages: Any) { + // Only log DEBUG if build type is DEBUG or if TESTING is true + if (BuildConfig.DEBUG || TESTING) { + log(tag, Log.DEBUG, null, *messages) + } + } + + fun i(tag: String, vararg messages: Any) { + log(tag, Log.INFO, null, *messages) + } + + fun w(tag: String, vararg messages: Any) { + log(tag, Log.WARN, null, *messages) + } + + fun w(tag: String, t: Throwable, vararg messages: Any) { + log(tag, Log.WARN, t, *messages) + } + + fun e(tag: String, vararg messages: Any) { + log(tag, Log.ERROR, null, *messages) + } + + fun e(tag: String, t: Throwable, vararg messages: Any) { + log(tag, Log.ERROR, t, *messages) + } + + private fun log(tag: String, level: Int, t: Throwable?, vararg messages: Any) { + val message: String + if (t == null && messages.size == 1) { + // handle this common case without the extra cost of creating a stringbuffer: + message = messages[0].toString() + } else { + val sb = StringBuilder() + for (m in messages) { + sb.append(m) + } + if (t != null) { + sb.append("\n").append(Log.getStackTraceString(t)) + } + message = sb.toString() + } + Log.println(level, tag, message) + +// if (Log.isLoggable(tag, level)) { +// val message: String +// if (t == null && messages != null && messages.size == 1) { +// // handle this common case without the extra cost of creating a stringbuffer: +// message = messages[0].toString() +// } else { +// val sb = StringBuilder() +// if (messages != null) +// for (m in messages) { +// sb.append(m) +// } +// if (t != null) { +// sb.append("\n").append(Log.getStackTraceString(t)) +// } +// message = sb.toString() +// } +// Log.println(level, tag, message) +// } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/MapHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/MapHelper.java deleted file mode 100755 index 67e1760..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/MapHelper.java +++ /dev/null @@ -1,275 +0,0 @@ -/** - * MapHelper.java - * Implements the MapHelper class - * A MapHelper offers helper methods for dealing with Trackbook's map - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.location.Location; -import android.widget.Toast; - -import org.osmdroid.util.BoundingBox; -import org.osmdroid.util.GeoPoint; -import org.osmdroid.views.overlay.ItemizedIconOverlay; -import org.osmdroid.views.overlay.OverlayItem; -import org.y20k.trackbook.R; -import org.y20k.trackbook.core.Track; -import org.y20k.trackbook.core.WayPoint; - -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; - - -/** - * MapHelper class - */ -public final class MapHelper implements TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = MapHelper.class.getSimpleName(); - - - /* Creates icon overlay for current position (used in MainActivity Fragment) */ - public static ItemizedIconOverlay createMyLocationOverlay(final Context context, Location currentBestLocation, boolean locationIsNew, boolean trackingActive) { - - final ArrayList overlayItems = new ArrayList<>(); - - // create marker - Drawable newMarker; - if (locationIsNew && !trackingActive) { - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_dot_blue_24dp); - } else if (!locationIsNew && trackingActive) { - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_dot_red_grey_24dp); - } else { - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_dot_blue_grey_24dp); - } - - OverlayItem overlayItem = createOverlayItem(context, currentBestLocation); - overlayItem.setMarker(newMarker); - - // add marker to list of overlay items - overlayItems.add(overlayItem); - - // create and return overlay for current position - return new ItemizedIconOverlay<>(overlayItems, - new ItemizedIconOverlay.OnItemGestureListener() { - @Override - public boolean onItemSingleTapUp(final int index, final OverlayItem item) { - // tap on My Location dot icon - Toast.makeText(context, item.getTitle() + " | " + item.getSnippet(), Toast.LENGTH_LONG).show(); - return true; - } - - @Override - public boolean onItemLongPress(final int index, final OverlayItem item) { - // long press on My Location dot icon - return true; - } - }, context); - } - - - /* Creates icon overlay for track */ - public static ItemizedIconOverlay createTrackOverlay(final Context context, Track track, boolean trackingActive){ - - final ArrayList overlayItems = new ArrayList<>(); - boolean currentPosition; - final int trackSize = track.getSize(); - final List wayPoints = track.getWayPoints(); - WayPoint wayPoint; - - for (int i = 0; i < track.getSize(); i++) { - - // get WayPoint and check if it is current position - wayPoint = wayPoints.get(i); - currentPosition = i == trackSize - 1; - - // create marker - Drawable newMarker; - - // CASE 1: Tracking active and WayPoint is not current position - if (trackingActive && !currentPosition) { - if (wayPoint.getIsStopOver()) { - // stop over marker - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_crumb_grey_24dp); - } else { - // default marker for this case - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_crumb_red_24dp); - } - } - - // CASE 2: Tracking active and WayPoint is current position - else if (trackingActive && currentPosition) { - if (wayPoint.getIsStopOver()) { - // stop over marker - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_dot_blue_grey_24dp); - } else { - // default marker for this case - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_dot_red_24dp); - } - } - - // CASE 3: Tracking not active and WayPoint is not current position - else if (!trackingActive && !currentPosition) { - if (wayPoint.getIsStopOver()) { - // stop over marker - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_crumb_grey_24dp); - } else { - // default marker for this case - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_crumb_blue_24dp); - } - } - - // CASE 4: Tracking not active and WayPoint is current position - else { - // default marker - newMarker = ContextCompat.getDrawable(context, R.drawable.ic_my_location_crumb_blue_24dp); - } - - // create overlay item - OverlayItem overlayItem = createOverlayItem(context, wayPoint.getLocation()); - overlayItem.setMarker(newMarker); - - // add marker to list of overlay items - overlayItems.add(overlayItem); - } - - // return overlay for current position - return new ItemizedIconOverlay<>(overlayItems, - new ItemizedIconOverlay.OnItemGestureListener() { - @Override - public boolean onItemSingleTapUp(final int index, final OverlayItem item) { - // tap on waypoint - Toast.makeText(context, item.getTitle(), Toast.LENGTH_LONG).show(); - return true; - } - - @Override - public boolean onItemLongPress(final int index, final OverlayItem item) { - // long press on waypoint - Toast.makeText(context, item.getSnippet(), Toast.LENGTH_LONG).show(); - return true; - } - - }, context); - } - - - /* Creates a marker overlay item */ - private static OverlayItem createOverlayItem(Context context, Location location) { - // create content of overlay item - String time = SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, Locale.getDefault()).format(location.getTime()); - final String title = context.getString(R.string.marker_description_source) + ": " + location.getProvider() + " | " + context.getString(R.string.marker_description_time) + ": " + time; - final String description = context.getString(R.string.marker_description_accuracy) + ": " + location.getAccuracy(); - final GeoPoint position = new GeoPoint(location.getLatitude(),location.getLongitude()); - - return new OverlayItem(title, description, position); - } - - - - /** - * Create a {@code BoundingBox} for the collection of - * {@code WayPoint}s, so that it would be possible to fit the map in - * such box and see the whole {@code Track} in the map without - * manual zooming. - * - * @return {@code BoundingBox} containing all {@code Waypoint}s - */ - public static BoundingBox calculateBoundingBox(List wayPoints) { - final ArrayList geoPoints = new ArrayList<>(wayPoints.size()); - - for (final WayPoint aWayPoint : wayPoints) { - final GeoPoint aGeoPoint = new GeoPoint(aWayPoint.getLocation()); - geoPoints.add(aGeoPoint); - } - return BoundingBox.fromGeoPoints(geoPoints).increaseByScale(1.15f); - } - - - - /* Calculates positive and negative elevation of track */ - public static Track calculateElevation(@Nullable Track track) { - double maxAltitude = 0; - double minAltitude = 0; - double positiveElevation = 0; - double negativeElevation = 0; - - if (track != null && track.getWayPoints().size() > 0) { - double previousLocationAltitude; - double currentLocationAltitude; - long previousTimeStamp; - long currentTimeStamp; - - // initial values for max height and min height - first waypoint - maxAltitude = track.getWayPointLocation(0).getAltitude(); - minAltitude = maxAltitude; - - // apply filter & smooth data -// track = smoothTrack(track, 15f, 35f); - - // iterate over track - for (int i = 1; i < track.getWayPoints().size(); i++ ) { - - // get time difference - previousTimeStamp = track.getWayPointLocation(i -1).getTime(); - currentTimeStamp = track.getWayPointLocation(i).getTime(); - double timeDiff = (currentTimeStamp - previousTimeStamp); - - // factor is bigger than 1 if the time stamp difference is larger than the movement recording interval (usually 15 seconds) - double timeDiffFactor = timeDiff / FIFTEEN_SECONDS_IN_MILLISECONDS; - - // height of previous and current waypoints - previousLocationAltitude = track.getWayPointLocation(i -1).getAltitude(); - currentLocationAltitude = track.getWayPointLocation(i).getAltitude(); - - // check for new min and max heights - if (currentLocationAltitude > maxAltitude) { - maxAltitude = currentLocationAltitude; - } - if (minAltitude == 0 || currentLocationAltitude < minAltitude) { - minAltitude = currentLocationAltitude; - } - - // get elevation difference and sum it up - double altitudeDiff = currentLocationAltitude - previousLocationAltitude; - if (altitudeDiff > 0 && altitudeDiff < MEASUREMENT_ERROR_THRESHOLD * timeDiffFactor && currentLocationAltitude != 0) { - positiveElevation = positiveElevation + altitudeDiff; - } - if (altitudeDiff < 0 && altitudeDiff > -MEASUREMENT_ERROR_THRESHOLD * timeDiffFactor && currentLocationAltitude != 0) { - negativeElevation = negativeElevation + altitudeDiff; - } - - } - - // store elevation data in track - track.setMaxAltitude(maxAltitude); - track.setMinAltitude(minAltitude); - track.setPositiveElevation(positiveElevation); - track.setNegativeElevation(negativeElevation); - } - return track; - } - - - - -} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/MapHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/MapHelper.kt new file mode 100644 index 0000000..1d191c8 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/MapHelper.kt @@ -0,0 +1,148 @@ +/* + * MapHelper.kt + * Implements the MapHelper object + * A MapHelper offers helper methods for manipulating osmdroid maps + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + + +import android.content.Context +import android.graphics.drawable.Drawable +import android.location.Location +import android.os.Vibrator +import android.widget.Toast +import androidx.core.content.ContextCompat +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.overlay.ItemizedIconOverlay +import org.osmdroid.views.overlay.OverlayItem +import org.y20k.trackbook.Keys +import org.y20k.trackbook.R +import org.y20k.trackbook.core.Track +import java.text.DecimalFormat +import java.text.SimpleDateFormat +import java.util.* + + +/* + * MapHelper object + */ +object MapHelper { + + /* Define log tag */ + private val LOG_TAG = MapHelper::class.java.simpleName + + + /* Creates icon overlay for current position (used in MapFragment) */ + fun createMyLocationOverlay(context: Context, location: Location, trackingState: Int): ItemizedIconOverlay { + + val overlayItems = ArrayList() + val locationIsOld = LocationHelper.isOldLocation(location) + + // create marker + val newMarker: Drawable + when (trackingState) { + // CASE: Tracking active + Keys.STATE_TRACKING_ACTIVE -> { + when (locationIsOld) { + true -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_location_red_grey_24dp)!! + false -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_location_red_24dp)!! + } + } + // CASE. Tracking is NOT active + else -> { + when (locationIsOld) { + true -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_location_blue_grey_24dp)!! + false -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_location_blue_24dp)!! + } + } + } + + // add marker to list of overlay items + val overlayItem = createOverlayItem(context, location.latitude, location.longitude, location.accuracy, location.provider, location.time) + overlayItem.setMarker(newMarker) + overlayItems.add(overlayItem) + + // create and return overlay for current position + return createOverlay(context, overlayItems) + } + + + /* Creates icon overlay for track */ + fun createTrackOverlay(context: Context, track: Track, trackingState: Int): ItemizedIconOverlay { + + val overlayItems = ArrayList() + val wayPoints = track.wayPoints + + wayPoints.forEach { wayPoint -> + // create marker + val newMarker: Drawable + + // get drawable + when (trackingState) { + // CASE: Recording is active + Keys.STATE_TRACKING_ACTIVE -> { + when (wayPoint.isStopOver) { + true -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_grey_24dp)!! + false -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_red_24dp)!! + } + } + // CASE: Recording is paused/stopped + else -> { + when (wayPoint.isStopOver) { + true -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_grey_24dp)!! + false -> newMarker = ContextCompat.getDrawable(context, R.drawable.ic_marker_track_location_blue_24dp)!! + } + } + } + + // create overlay item and add to list of overlay items + val overlayItem = createOverlayItem(context, wayPoint.latitude, wayPoint.longitude, wayPoint.accuracy, wayPoint.provider, wayPoint.time) + overlayItem.setMarker(newMarker) + overlayItems.add(overlayItem) + } + + // create and return overlay for current position + return createOverlay(context, overlayItems) + } + + + /* Creates a marker overlay item */ + private fun createOverlayItem(context: Context, latitude: Double, longitude: Double, accuracy: Float, provider: String, time: Long): OverlayItem { + val title: String = "${context.getString(R.string.marker_description_time)}: ${SimpleDateFormat.getTimeInstance(SimpleDateFormat.MEDIUM, Locale.getDefault()).format(time)}" + val description: String = "${context.getString(R.string.marker_description_accuracy)}: ${DecimalFormat("#0.00").format(accuracy)} (${provider})" + val position: GeoPoint = GeoPoint(latitude, longitude) + return OverlayItem(title, description, position) + } + + + /* Creates an overlay */ + private fun createOverlay(context: Context, overlayItems: ArrayList): ItemizedIconOverlay { + return ItemizedIconOverlay(overlayItems, + object : ItemizedIconOverlay.OnItemGestureListener { + override fun onItemSingleTapUp(index: Int, item: OverlayItem): Boolean { + Toast.makeText(context, item.title, Toast.LENGTH_LONG).show() + return true + } + override fun onItemLongPress(index: Int, item: OverlayItem): Boolean { + val v = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + v.vibrate(50) + Toast.makeText(context, item.snippet, Toast.LENGTH_LONG).show() + return true + } + }, context) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.java deleted file mode 100755 index 4b8131c..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.java +++ /dev/null @@ -1,177 +0,0 @@ -/** - * NightModeHelper.java - * Implements the NightModeHelper class - * A NightModeHelper can toggle and restore the state of the theme's Night Mode - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.os.Build; -import android.preference.PreferenceManager; -import android.view.View; -import android.widget.Toast; - -import androidx.appcompat.app.AppCompatDelegate; - -import org.y20k.trackbook.R; - - -/** - * NightModeHelper class - */ -public final class NightModeHelper implements TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = NightModeHelper.class.getSimpleName(); - - - /* Switches between modes: day, night, undefined */ - public static void switchMode(Activity activity) { - // SWITCH: undefined -> night / night -> day / day - undefined - switch (AppCompatDelegate.getDefaultNightMode()) { - case AppCompatDelegate.MODE_NIGHT_NO: - // currently: day mode -> switch to: follow system - displayDefaultStatusBar(activity); // necessary hack :-/ - activateFollowSystemMode(activity, true); - break; - case AppCompatDelegate.MODE_NIGHT_YES: - // currently: night mode -> switch to: day mode - displayLightStatusBar(activity); // necessary hack :-/ - activateDayMode(activity, true); - break; - default: - // currently: follow system / undefined -> switch to: day mode - displayLightStatusBar(activity); // necessary hack :-/ - activateNightMode(activity, true); - break; - } - } - - - /* Sets night mode / dark theme */ - public static void restoreSavedState(Context context) { - int savedNightModeState = loadNightModeState(context); - int currentNightModeState = AppCompatDelegate.getDefaultNightMode(); - if (savedNightModeState != currentNightModeState) { - switch (savedNightModeState) { - case AppCompatDelegate.MODE_NIGHT_NO: - // turn on day mode - activateDayMode(context, false); - break; - case AppCompatDelegate.MODE_NIGHT_YES: - // turn on night mode - activateNightMode(context, false); - break; - default: - // turn on mode "follow system" - activateFollowSystemMode(context, false); - break; - } - } - } - - - /* Return weather Night Mode is on, or not */ - public static Boolean getNightMode(Context context) { - int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - return nightMode == Configuration.UI_MODE_NIGHT_YES; - } - - - /* Returns state of night mode */ - private static int getCurrentNightModeState(Context context) { - return context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - } - - - /* Activates Night Mode */ - private static void activateNightMode(Context context, Boolean notifyUser) { - saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_YES); - - // switch to Night Mode - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); - - // notify user - if (notifyUser) { - Toast.makeText(context, context.getText(R.string.toast_message_theme_night), Toast.LENGTH_SHORT).show(); - } - } - - - /* Activates Day Mode */ - private static void activateDayMode(Context context, Boolean notifyUser) { - // save the new state - saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_NO); - - // switch to Day Mode - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); - - // notify user - if (notifyUser) { - Toast.makeText(context, context.getText(R.string.toast_message_theme_day), Toast.LENGTH_LONG).show(); - } - } - - - /* Activate Mode "Follow System" */ - private static void activateFollowSystemMode(Context context, Boolean notifyUser) { - // save the new state - saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); - - // switch to Undefined Mode / Follow System - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); - - // notify user - if (notifyUser) { - Toast.makeText(context, context.getText(R.string.toast_message_theme_follow_system), Toast.LENGTH_LONG).show(); - } - } - - - /* Displays the default status bar */ - private static void displayDefaultStatusBar(Activity activity) { - View decorView = activity.getWindow().getDecorView(); - decorView.setSystemUiVisibility(0); - } - - - /* Displays the light (inverted) status bar - if possible */ - private static void displayLightStatusBar(Activity activity) { - View decorView = activity.getWindow().getDecorView(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); - } else { - decorView.setSystemUiVisibility(0); - } - } - - - /* Save state of night mode */ - private static void saveNightModeState(Context context, int currentState) { - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); - SharedPreferences.Editor editor = settings.edit(); - editor.putInt(PREF_NIGHT_MODE_STATE, currentState); - editor.apply(); - } - - - /* Load state of Night Mode */ - private static int loadNightModeState(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getInt(PREF_NIGHT_MODE_STATE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.kt new file mode 100644 index 0000000..594d131 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/NightModeHelper.kt @@ -0,0 +1,150 @@ +/* + * NightModeHelper.kt + * Implements the NightModeHelper object + * A NightModeHelper can toggle and restore the state of the theme's Night Mode + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.res.Configuration +import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AppCompatDelegate +import org.y20k.trackbook.R + + +/* + * NightModeHelper object + */ +object NightModeHelper { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(NightModeHelper::class.java) + + + /* Switches between modes: day, night, undefined */ + @SuppressLint("SwitchIntDef") + fun switchMode(activity: Activity) { + // SWITCH: undefined -> night / night -> day / day - undefined + when (AppCompatDelegate.getDefaultNightMode()) { + AppCompatDelegate.MODE_NIGHT_NO -> { + // currently: day mode -> switch to: follow system + // displayDefaultStatusBar(activity) // necessary hack :-/ + activateFollowSystemMode(activity, true) + } + AppCompatDelegate.MODE_NIGHT_YES -> { + // currently: night mode -> switch to: day mode + // displayLightStatusBar(activity) // necessary hack :-/ + activateDayMode(activity, true) + } + else -> { + // currently: follow system / undefined -> switch to: day mode + // displayLightStatusBar(activity) // necessary hack :-/ + activateNightMode(activity, true) + } + } + } + + + /* Sets night mode / dark theme */ + fun restoreSavedState(context: Context) { + val savedNightModeState = PreferencesHelper.loadNightModeState(context) + val currentNightModeState = AppCompatDelegate.getDefaultNightMode() + if (savedNightModeState != currentNightModeState) { + when (savedNightModeState) { + AppCompatDelegate.MODE_NIGHT_NO -> + // turn on day mode + activateDayMode(context, false) + AppCompatDelegate.MODE_NIGHT_YES -> + // turn on night mode + activateNightMode(context, false) + else -> + // turn on mode "follow system" + activateFollowSystemMode(context, false) + } + } + } + + + /* Return weather Night Mode is on, or not */ + fun isNightModeOn(context: Context): Boolean { + val nightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return nightMode == Configuration.UI_MODE_NIGHT_YES + } + + + /* Activates Night Mode */ + private fun activateNightMode(context: Context, notifyUser: Boolean) { + PreferencesHelper.saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_YES) + + // switch to Night Mode + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + + // notify user + if (notifyUser) { + Toast.makeText(context, context.getText(R.string.toast_message_theme_night), Toast.LENGTH_LONG).show() + } + } + + + /* Activates Day Mode */ + private fun activateDayMode(context: Context, notifyUser: Boolean) { + // save the new state + PreferencesHelper.saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_NO) + + // switch to Day Mode + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + + // notify user + if (notifyUser) { + Toast.makeText(context, context.getText(R.string.toast_message_theme_day), Toast.LENGTH_LONG).show() + } + } + + + /* Activate Mode "Follow System" */ + private fun activateFollowSystemMode(context: Context, notifyUser: Boolean) { + // save the new state + PreferencesHelper.saveNightModeState(context, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + + // switch to Undefined Mode / Follow System + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + + // notify user + if (notifyUser) { + Toast.makeText(context, context.getText(R.string.toast_message_theme_follow_system), Toast.LENGTH_LONG).show() + } + } + + + /* Displays the default status bar */ + private fun displayDefaultStatusBar(activity: Activity) { + val decorView = activity.window.decorView + decorView.systemUiVisibility = 0 + } + + + /* Displays the light (inverted) status bar */ + private fun displayLightStatusBar(activity: Activity) { + val decorView = activity.window.decorView + decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } + + + + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.java deleted file mode 100755 index a86d277..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.java +++ /dev/null @@ -1,171 +0,0 @@ -/** - * NotificationHelper.java - * Implements the NotificationHelper class - * A NotificationHelper creates and configures a notification - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.os.Build; - -import org.y20k.trackbook.MainActivity; -import org.y20k.trackbook.R; -import org.y20k.trackbook.TrackerService; -import org.y20k.trackbook.core.Track; - -import androidx.core.app.NotificationCompat; -import androidx.core.app.TaskStackBuilder; -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; - - -/** - * NotificationHelper class - */ -public final class NotificationHelper implements TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = NotificationHelper.class.getSimpleName(); - - - /* Creates a notification builder */ - public static Notification getNotification(Context context, NotificationCompat.Builder builder, Track track, boolean tracking) { - - // create notification channel - createNotificationChannel(context); - - // ACTION: NOTIFICATION TAP & BUTTON SHOW - Intent tapActionIntent = new Intent(context, MainActivity.class); - tapActionIntent.setAction(ACTION_SHOW_MAP); - tapActionIntent.putExtra(EXTRA_TRACK, track); - tapActionIntent.putExtra(EXTRA_TRACKING_STATE, tracking); - // artificial back stack for started Activity (https://developer.android.com/training/notify-user/navigation.html#DirectEntry) - TaskStackBuilder tapActionIntentBuilder = TaskStackBuilder.create(context); - tapActionIntentBuilder.addParentStack(MainActivity.class); - tapActionIntentBuilder.addNextIntent(tapActionIntent); - // pending intent wrapper for notification tap - PendingIntent tapActionPendingIntent = tapActionIntentBuilder.getPendingIntent(10, PendingIntent.FLAG_UPDATE_CURRENT); - - // ACTION: NOTIFICATION BUTTON STOP - Intent stopActionIntent = new Intent(context, TrackerService.class); - stopActionIntent.setAction(ACTION_STOP); - // pending intent wrapper for notification stop action - PendingIntent stopActionPendingIntent = PendingIntent.getService(context, 14, stopActionIntent, 0); - - // ACTION: NOTIFICATION BUTTON RESUME - Intent resumeActionIntent = new Intent(context, TrackerService.class); - resumeActionIntent.setAction(ACTION_RESUME); - // pending intent wrapper for notification resume action - PendingIntent resuneActionPendingIntent = PendingIntent.getService(context, 16, resumeActionIntent, 0); - - // construct notification in builder - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - builder.setShowWhen(false); - builder.setContentIntent(tapActionPendingIntent); - builder.setSmallIcon(R.drawable.ic_notification_small_24dp); - builder.setLargeIcon(getNotificationIconLarge(context, tracking)); - if (tracking) { - builder.addAction(R.drawable.ic_stop_white_24dp, context.getString(R.string.notification_stop), stopActionPendingIntent); - builder.setContentTitle(context.getString(R.string.notification_title_trackbook_running)); - builder.setContentText(getContextString(context, track)); - } else { - builder.addAction(R.drawable.ic_fiber_manual_record_white_24dp, context.getString(R.string.notification_resume), resuneActionPendingIntent); - builder.addAction(R.drawable.ic_compass_needle_white_24dp, context.getString(R.string.notification_show), tapActionPendingIntent); - builder.setContentTitle(context.getString(R.string.notification_title_trackbook_not_running)); - builder.setContentText(getContextString(context, track)); - } - - return builder.build(); - } - - - /* Constructs an updated notification */ - public static Notification getUpdatedNotification(Context context, NotificationCompat.Builder builder, Track track) { - builder.setContentText(getContextString(context, track)); - return builder.build(); - } - - - /* Create a notification channel */ - public static boolean createNotificationChannel(Context context) { - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // API level 26 ("Android O") supports notification channels. - String id = NOTIFICATION_CHANEL_ID_RECORDING_CHANNEL; - CharSequence name = context.getString(R.string.notification_channel_recording_name); - String description = context.getString(R.string.notification_channel_recording_description); - int importance = NotificationManager.IMPORTANCE_LOW; - - // create channel - NotificationChannel channel = new NotificationChannel(id, name, importance); - channel.setDescription(description); - - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.createNotificationChannel(channel); - return true; - - } else { - return false; - } - } - - - /* Get station image for notification's large icon */ - private static Bitmap getNotificationIconLarge(Context context, boolean tracking) { - - // get dimensions - Resources resources = context.getResources(); - int height = (int) resources.getDimension(android.R.dimen.notification_large_icon_height); - int width = (int) resources.getDimension(android.R.dimen.notification_large_icon_width); - - Bitmap bitmap; - if (tracking) { - bitmap = getBitmap(context, R.drawable.ic_notification_large_tracking_48dp); - } else { - bitmap = getBitmap(context, R.drawable.ic_notification_large_not_tracking_48dp); - } - - return Bitmap.createScaledBitmap(bitmap, width, height, false); - } - - - /* Return a bitmap for a given resource id of a vector drawable */ - private static Bitmap getBitmap(Context context, int resource) { - VectorDrawableCompat drawable = VectorDrawableCompat.create(context.getResources(), resource, null); - if (drawable != null) { - Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - return bitmap; - } else { - return null; - } - } - - - /* Build context text for notification builder */ - private static String getContextString(Context context, Track track) { - return context.getString(R.string.notification_content_distance) + ": " + LengthUnitHelper.convertDistanceToString(track.getTrackDistance()) + " | " + - context.getString(R.string.notification_content_duration) + ": " + LocationHelper.convertToReadableTime(track.getTrackDuration(), true); - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.kt new file mode 100644 index 0000000..2f2fe02 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/NotificationHelper.kt @@ -0,0 +1,135 @@ +/* + * NotificationHelper.kt + * Implements the NotificationHelper class + * A NotificationHelper creates and configures a notification + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.app.* +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.app.NotificationCompat +import androidx.core.graphics.drawable.toBitmap +import org.y20k.trackbook.Keys +import org.y20k.trackbook.MainActivity +import org.y20k.trackbook.R +import org.y20k.trackbook.TrackerService + + +/* + * NotificationHelper class + */ +class NotificationHelper(private val trackerService: TrackerService) { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(NotificationHelper::class.java) + + + + /* Main class variables */ + private val notificationManager: NotificationManager = trackerService.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + + /* Creates notification */ + fun createNotification(trackingState: Int, trackLength: Float, duration: Long, useImperial: Boolean): Notification { + + // create notification channel if necessary + if (shouldCreateNotificationChannel()) { + createNotificationChannel() + } + + // Build notification + val builder = NotificationCompat.Builder(trackerService, Keys.NOTIFICATION_CHANNEL_RECORDING) + builder.setContentIntent(showActionPendingIntent) + builder.setSmallIcon(R.drawable.ic_notification_icon_small_24dp) + builder.setContentText(getContentString(trackerService, duration, trackLength, useImperial)) + + // add icon and actions for stop, resume and show + when (trackingState) { + Keys.STATE_TRACKING_ACTIVE -> { + builder.setContentTitle(trackerService.getString(R.string.notification_title_trackbook_running)) + builder.addAction(stopAction) + builder.setLargeIcon(AppCompatResources.getDrawable(trackerService, R.drawable.ic_notification_icon_large_tracking_active_48dp)!!.toBitmap()) + } + else -> { + builder.setContentTitle(trackerService.getString(R.string.notification_title_trackbook_not_running)) + builder.addAction(resumeAction) + builder.addAction(showAction) + builder.setLargeIcon(AppCompatResources.getDrawable(trackerService, R.drawable.ic_notification_icon_large_tracking_stopped_48dp)!!.toBitmap()) + } + } + + return builder.build() + + } + + + /* Build context text for notification builder */ + private fun getContentString(context: Context, duration: Long, trackLength: Float, useImperial: Boolean): String { + return "${LengthUnitHelper.convertDistanceToString(trackLength, useImperial)} • ${DateTimeHelper.convertToReadableTime(context, duration)}" + } + + + /* Checks if notification channel should be created */ + private fun shouldCreateNotificationChannel() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !nowPlayingChannelExists() + + + /* Checks if notification channel exists */ + @RequiresApi(Build.VERSION_CODES.O) + private fun nowPlayingChannelExists() = notificationManager.getNotificationChannel(Keys.NOTIFICATION_CHANNEL_RECORDING) != null + + + /* Create a notification channel */ + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel() { + val notificationChannel = NotificationChannel(Keys.NOTIFICATION_CHANNEL_RECORDING, + trackerService.getString(R.string.notification_channel_recording_name), + NotificationManager.IMPORTANCE_LOW) + .apply { description = trackerService.getString(R.string.notification_channel_recording_description) } + notificationManager.createNotificationChannel(notificationChannel) + } + + + /* Notification pending intents */ + private val stopActionPendingIntent = PendingIntent.getService( + trackerService,14, + Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_STOP),0) + private val resumeActionPendingIntent = PendingIntent.getService( + trackerService, 16, + Intent(trackerService, TrackerService::class.java).setAction(Keys.ACTION_RESUME),0) + private val showActionPendingIntent: PendingIntent? = TaskStackBuilder.create(trackerService).run { + addNextIntentWithParentStack(Intent(trackerService, MainActivity::class.java)) + getPendingIntent(10, PendingIntent.FLAG_UPDATE_CURRENT) + } + + + /* Notification actions */ + private val stopAction = NotificationCompat.Action( + R.drawable.ic_notification_action_stop_24dp, + trackerService.getString(R.string.notification_stop), + stopActionPendingIntent) + private val resumeAction = NotificationCompat.Action( + R.drawable.ic_notification_action_resume_36dp, + trackerService.getString(R.string.notification_resume), + resumeActionPendingIntent) + private val showAction = NotificationCompat.Action( + R.drawable.ic_notification_action_show_36dp, + trackerService.getString(R.string.notification_show), + showActionPendingIntent) + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/PreferencesHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/PreferencesHelper.kt new file mode 100644 index 0000000..5ab9774 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/PreferencesHelper.kt @@ -0,0 +1,167 @@ +/* + * PreferencesHelper.kt + * Implements the PreferencesHelper object + * A PreferencesHelper provides helper methods for the saving and loading values from shared preferences + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.content.Context +import android.location.Location +import android.location.LocationManager +import androidx.appcompat.app.AppCompatDelegate +import androidx.preference.PreferenceManager +import org.y20k.trackbook.Keys +import org.y20k.trackbook.extensions.getDouble +import org.y20k.trackbook.extensions.putDouble + + +/* + * PreferencesHelper object + */ +object PreferencesHelper { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(PreferencesHelper::class.java) + + + /* Loads zoom level of map */ + fun loadZoomLevel(context: Context): Double { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + // load zoom level + return settings.getDouble(Keys.PREF_MAP_ZOOM_LEVEL, Keys.DEFAULT_ZOOM_LEVEL) + } + + + /* Saves zoom level of map */ + fun saveZoomLevel(context: Context, zoomLevel: Double) { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val editor = settings.edit() + // save zoom level + editor.putDouble(Keys.PREF_MAP_ZOOM_LEVEL, zoomLevel) + editor.apply() + } + + + /* Loads tracking state */ + fun loadTrackingState(context: Context): Int { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + // load tracking state + return settings.getInt(Keys.PREF_TRACKING_STATE, Keys.STATE_NOT_TRACKING) + } + + + /* Saves tracking state */ + fun saveTrackingState(context: Context, trackingState: Int) { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val editor = settings.edit() + // save tracking state + editor.putInt(Keys.PREF_TRACKING_STATE, trackingState) + editor.apply() + } + + + /* Loads length unit system - metric or imperial */ + fun loadUseImperialUnits(context: Context): Boolean { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + // load length unit system + return settings.getBoolean(Keys.PREF_USE_IMPERIAL_UNITS, LengthUnitHelper.useImperialUnits()) + } + + + /* Loads length unit system - metric or imperial */ + fun loadGpsOnly(context: Context): Boolean { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + // load length unit system + return settings.getBoolean(Keys.PREF_GPS_ONLY, false) + } + + /* Loads accuracy threshold used to determine if location is good enough */ + fun loadAccuracyThreshold(context: Context): Int { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + // load tracking state + return settings.getInt(Keys.PREF_LOCATION_ACCURACY_THRESHOLD, Keys.DEFAULT_THRESHOLD_LOCATION_ACCURACY) + } + + + /* Loads the state of a map */ + fun loadCurrentBestLocation(context: Context): Location { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val provider: String = settings.getString(Keys.PREF_CURRENT_BEST_LOCATION_PROVIDER, LocationManager.NETWORK_PROVIDER) ?: LocationManager.NETWORK_PROVIDER + // create location + val currentBestLocation: Location = Location(provider) + // load location attributes + currentBestLocation.latitude = settings.getDouble(Keys.PREF_CURRENT_BEST_LOCATION_LATITUDE, Keys.DEFAULT_LATITUDE) + currentBestLocation.longitude = settings.getDouble(Keys.PREF_CURRENT_BEST_LOCATION_LONGITUDE, Keys.DEFAULT_LONGITUDE) + currentBestLocation.accuracy = settings.getFloat(Keys.PREF_CURRENT_BEST_LOCATION_ACCURACY, Keys.DEFAULT_ACCURACY) + currentBestLocation.altitude = settings.getDouble(Keys.PREF_CURRENT_BEST_LOCATION_ALTITUDE, Keys.DEFAULT_ALTITUDE) + currentBestLocation.time = settings.getLong(Keys.PREF_CURRENT_BEST_LOCATION_TIME, Keys.DEFAULT_TIME) + return currentBestLocation + } + + + /* Saves the state of a map */ + fun saveCurrentBestLocation(context: Context, currentBestLocation: Location) { + // get preferences + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val editor = settings.edit() + // save location + editor.putDouble(Keys.PREF_CURRENT_BEST_LOCATION_LATITUDE, currentBestLocation.latitude) + editor.putDouble(Keys.PREF_CURRENT_BEST_LOCATION_LONGITUDE, currentBestLocation.longitude) + editor.putFloat(Keys.PREF_CURRENT_BEST_LOCATION_ACCURACY, currentBestLocation.accuracy) + editor.putDouble(Keys.PREF_CURRENT_BEST_LOCATION_ALTITUDE, currentBestLocation.altitude) + editor.putLong(Keys.PREF_CURRENT_BEST_LOCATION_TIME, currentBestLocation.time) + editor.apply() + } + + + /* Load state of Night Mode */ + fun loadNightModeState(context: Context): Int { + return PreferenceManager.getDefaultSharedPreferences(context).getInt(Keys.PREF_NIGHT_MODE_STATE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + } + + + /* Save state of night mode */ + fun saveNightModeState(context: Context, currentState: Int) { + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val editor = settings.edit() + editor.putInt(Keys.PREF_NIGHT_MODE_STATE, currentState) + editor.apply() + } + + + /* Checks if housekeeping work needs to be done - used usually in DownloadWorker "REQUEST_UPDATE_COLLECTION" */ + fun isHouseKeepingNecessary(context: Context): Boolean { + val settings = PreferenceManager.getDefaultSharedPreferences(context) + return settings.getBoolean(Keys.PREF_ONE_TIME_HOUSEKEEPING_NECESSARY, true) + } + + + /* Saves state of housekeeping */ + fun saveHouseKeepingNecessaryState(context: Context, state: Boolean = false) { + val settings = PreferenceManager.getDefaultSharedPreferences(context) + val editor = settings.edit() + editor.putBoolean(Keys.PREF_ONE_TIME_HOUSEKEEPING_NECESSARY, state) + editor.apply() + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/StorageHelper.java b/app/src/main/java/org/y20k/trackbook/helpers/StorageHelper.java deleted file mode 100755 index 400b340..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/StorageHelper.java +++ /dev/null @@ -1,405 +0,0 @@ -/** - * StorageHelper.java - * Implements the StorageHelper class - * A StorageHelper deals with saving and loading recorded tracks - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - -import android.content.Context; -import android.os.Environment; -import android.widget.Toast; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import org.y20k.trackbook.R; -import org.y20k.trackbook.core.Track; -import org.y20k.trackbook.core.TrackBuilder; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Date; -import java.util.Locale; - -import androidx.annotation.Nullable; -import androidx.core.os.EnvironmentCompat; - - -/** - * StorageHelper class - */ -public class StorageHelper implements TrackbookKeys { - - /* Define log tag */ - private static final String LOG_TAG = StorageHelper.class.getSimpleName(); - - /* Main class variables */ - private final Context mContext; - private final File mFolder; - private final File mTempFile; - - - /* Constructor */ - public StorageHelper(Context context) { - // store activity - mContext = context; - - // get "tracks" folder - mFolder = mContext.getExternalFilesDir(TRACKS_DIRECTORY_NAME); - // mFolder = getTracksDirectory(); - - // create "tracks" folder if necessary - if (mFolder != null && !mFolder.exists()) { - LogHelper.v(LOG_TAG, "Creating new folder: " + mFolder.toString()); - mFolder.mkdirs(); - } - - // create temp file object // todo check -> may produce NullPointerException - String tempFilePathName = mFolder.toString() + "/" + FILE_NAME_TEMP + FILE_TYPE_TRACKBOOK_EXTENSION; - mTempFile = new File(tempFilePathName); - - // delete old track - exclude temp file - deleteOldTracks(false); - } - - - /* Checks if a temp file exits */ - public boolean tempFileExists() { - return mTempFile.exists(); - } - - - /* Deletes temp file - if it exits */ - public boolean deleteTempFile() { - return mTempFile.exists() && mTempFile.delete(); - } - - - /* Saves track object to file */ - public boolean saveTrack(@Nullable Track track, int fileType) { - - Date recordingStart = null; - if (track != null) { - recordingStart = track.getRecordingStart(); - } - - if (mFolder != null && mFolder.exists() && mFolder.isDirectory() && mFolder.canWrite() && recordingStart != null && track != null) { - - // create file object and calculate bounding box and elevation, if necessary - String fileName; - if (fileType == FILE_TEMP_TRACK) { - // get the temp file name - fileName = FILE_NAME_TEMP + FILE_TYPE_TRACKBOOK_EXTENSION; - } else { - // build a regular file name - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US); - fileName = dateFormat.format(recordingStart) + FILE_TYPE_TRACKBOOK_EXTENSION; - // calculate elevation and store it in track - track = MapHelper.calculateElevation(track); - // calculate bounding box and store it in track - track.setBoundingBox(MapHelper.calculateBoundingBox(track.getWayPoints())); - } - File file = new File(mFolder.toString() + "/" + fileName); - - // convert track to JSON - Gson gson = getCustomGson(); - String json = gson.toJson(track); - - // write track - try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) { - LogHelper.v(LOG_TAG, "Saving track to external storage: " + file.toString()); - bw.write(json); - } catch (IOException e) { - LogHelper.e(LOG_TAG, "Unable to saving track to external storage (IOException): " + file.toString()); - return false; - } - - // if write was successful delete old track files - only if not a temp file - if (fileType != FILE_TEMP_TRACK) { - // include temp file if it exists - deleteOldTracks(true); - } - - return true; - - } else { - LogHelper.e(LOG_TAG, "Unable to save track to external storage."); - return false; - } - - } - - - /* Loads given file into memory */ - public Track loadTrack(int fileType) { - - // get file reference - File trackFile; - switch (fileType) { - case FILE_TEMP_TRACK: - trackFile = getTempFile(); - break; - case FILE_MOST_CURRENT_TRACK: - trackFile = getMostCurrentTrack(); - break; - default: - trackFile = null; - break; - } - - // read & parse file and return track - return readTrackFromFile(trackFile); - } - - - /* Loads given file into memory */ - public Track loadTrack(File file) { - - // get file reference - File trackFile; - if (file != null) { - trackFile = file; - } else { - // fallback - trackFile = getMostCurrentTrack(); - } - - // read & parse file and return track - return readTrackFromFile(trackFile); - } - - - /* Gets a list of .trackbook files - excluding the temp file */ - public File[] getListOfTrackbookFiles() { - // TODO HANDLE CASE: EMPTY FILE LIST - - // get files and sort them - return sortFiles(mFolder.listFiles()); - } - - -// /* Gets a list of tracks based on their file names */ -// public List getListOfTracks() { -// List listOfTracks = new ArrayList(); -// -// // get files and sort them -// File[] files = mFolder.listFiles(); -// files = sortFiles(files); -// -// for (File file : files) { -// listOfTracks.add(file.getName()); -// } -// -// // TODO HANDLE CASE: EMPTY FILE LIST -// return listOfTracks; -// } - - - // loads file and parses it into a track - private Track readTrackFromFile(File file) { - - // check if given file was null - if (file == null) { - LogHelper.e(LOG_TAG, "Did not receive a file object."); - return null; - } - - try (BufferedReader br = new BufferedReader(new FileReader(file))) { - LogHelper.v(LOG_TAG, "Loading track from external storage: " + file.toString()); - - // read until last line reached - String fileContent; - String singleLine; - StringBuilder sb = new StringBuilder(""); - while ((singleLine = br.readLine()) != null) { - sb.append(singleLine); - sb.append("\n"); - } - fileContent = sb.toString(); - - // prepare custom Gson and return Track object - Gson gson = getCustomGson(); - return gson.fromJson(fileContent, TrackBuilder.class).toTrack(); - - } catch (IOException e) { - LogHelper.e(LOG_TAG, "Unable to read file from external storage: " + file.toString()); - return null; - } - } - - - /* Creates a Gson object */ - private Gson getCustomGson() { - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.setDateFormat("M/d/yy hh:mm a"); - return gsonBuilder.create(); - } - - /* Gets most current track from directory */ - private File getMostCurrentTrack() { - - if (mFolder != null && mFolder.isDirectory()) { - // get files and sort them - File[] files = mFolder.listFiles(); - files = sortFiles(files); - if (files.length > 0 && files[0].getName().endsWith(FILE_TYPE_TRACKBOOK_EXTENSION) && !files[0].equals(mTempFile)){ - // return latest track - return files[0]; - } - } - LogHelper.e(LOG_TAG, "Unable to get files from given folder. Folder is probably empty."); - return null; - } - - - /* Gets temp file - if it exists */ - private File getTempFile() { - if (mTempFile.exists()) { - return mTempFile; - } else { - return null; - } - } - - - /* Gets the last track from directory */ - private void deleteOldTracks(boolean includeTempFile) { - - if (mFolder != null && mFolder.isDirectory()) { - LogHelper.v(LOG_TAG, "Deleting older recordings."); - - // get files and sort them - File[] files = mFolder.listFiles(); - files = sortFiles(files); - - // store length of array - int numberOfFiles = files.length; - - // keep the latest ten (mMaxTrackFiles) track files - int index = MAXIMUM_TRACK_FILES; - // iterate through array - while (index < numberOfFiles && files[index].getName().endsWith(FILE_TYPE_TRACKBOOK_EXTENSION) && !files[index].equals(mTempFile)) { - files[index].delete(); - index++; - } - } - - // delete temp file if it exists - if (includeTempFile && mTempFile.exists()) { - mTempFile.delete(); - } - - } - - - /* Sorts array of files in a way that the newest files are at the top and non-.trackbook files are at the bottom */ - private File[] sortFiles(File[] files) { - // sort array - LogHelper.v(LOG_TAG, "Sorting files."); - Arrays.sort(files, new Comparator() { - @Override - public int compare(File file1, File file2) { - - // discard temp file and files not ending with ".trackbook" - boolean file1IsTrack = file1.getName().endsWith(FILE_TYPE_TRACKBOOK_EXTENSION) && !file1.equals(mTempFile); - boolean file2IsTrack = file2.getName().endsWith(FILE_TYPE_TRACKBOOK_EXTENSION) && !file2.equals(mTempFile); - - // note: "greater" means higher index in array - if (!file1IsTrack && file2IsTrack) { - // file1 is not a track, file1 is greater - return 1; - } else if (!file2IsTrack && file1IsTrack) { - // file2 is not a track, file2 is greater - return -1; - } else { - // "compareTo" compares abstract path names lexicographically | 0 == equal | -1 == file2 less than file1 | 1 == file2 greater than file1 - return file2.compareTo(file1); - } - - } - }); - - // hand back sorted array of files - return files; - } - - - /* Return a write-able sub-directory from external storage */ - private File getTracksDirectory() { - File[] storage = mContext.getExternalFilesDirs(TRACKS_DIRECTORY_NAME); - for (File file : storage) { - if (file != null) { - String state = EnvironmentCompat.getStorageState(file); - if (Environment.MEDIA_MOUNTED.equals(state)) { - LogHelper.i(LOG_TAG, "External storage: " + file.toString()); - return file; - } - } - } - Toast.makeText(mContext, R.string.toast_message_no_external_storage, Toast.LENGTH_LONG).show(); - LogHelper.e(LOG_TAG, "Unable to access external storage."); - - return null; - } - - - /* Tries to smooth the elevation data using a low pass filter */ - private Track smoothTrack(Track input, float dt, float rc) { - - // The following code is adapted from https://en.wikipedia.org/wiki/Low-pass_filter - // - // // Return RC low-pass filter output samples, given input samples, - // // time interval dt, and time constant RC - // function lowpass(real[0..n] x, real dt, real RC) - // var real[0..n] y - // var real α := dt / (RC + dt) - // y[0] := α * x[0] - // for i from 1 to n - // y[i] := α * x[i] + (1-α) * y[i-1] - // return y - - // copy input track - Track output = new Track(input); - - // calculate alpha - float alpha = dt / (rc + dt); - - // set initial value for first waypoint - double outputInitialAltitudeValue = alpha * input.getWayPoints().get(0).getLocation().getAltitude(); - output.getWayPoints().get(0).getLocation().setAltitude(outputInitialAltitudeValue); - - double inputCurrentAltitudeValue; - double outputPreviousAltitudeValue; - double outputCurrentAltitudeValue; - for (int i = 1; i < input.getSize(); i++) { - inputCurrentAltitudeValue = input.getWayPoints().get(i).getLocation().getAltitude(); - outputPreviousAltitudeValue = output.getWayPoints().get(i-1).getLocation().getAltitude(); - - outputCurrentAltitudeValue = alpha * inputCurrentAltitudeValue + (1 - alpha) * outputPreviousAltitudeValue; - - output.getWayPoints().get(i).getLocation().setAltitude(outputCurrentAltitudeValue); - } - - return output; - } - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt new file mode 100644 index 0000000..00d073f --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/TrackHelper.kt @@ -0,0 +1,194 @@ +/* + * TrackHelper.kt + * Implements the TrackHelper object + * A TrackHelper offers helper methods for dealing with track objects + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.helpers + +import android.location.Location +import org.y20k.trackbook.core.Track +import org.y20k.trackbook.core.TracklistElement +import org.y20k.trackbook.core.WayPoint +import java.text.SimpleDateFormat +import java.util.* + + +/* + * TrackHelper object + */ +object TrackHelper { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(TrackHelper::class.java) + + + /* Returns unique ID for Track - currently the start date */ + fun getTrackId(track: Track): Long { + return track.recordingStart.time + } + + + /* Returns unique ID for TracklistElement - currently the start date */ + fun getTrackId(tracklistElement: TracklistElement): Long { + return tracklistElement.date.time + } + + + /* Adds given locatiom as waypoint to track */ + fun addWayPointToTrack(track: Track, location: Location, locationAccuracyThreshold: Int): Track { + + // get previous location + val previousLocation: Location? + val numberOfWayPoints: Int = track.wayPoints.size + if (numberOfWayPoints == 0) { + previousLocation = null + } else { + previousLocation = track.wayPoints.get(numberOfWayPoints - 1).toLocation() + } + + // update duration + val now: Date = GregorianCalendar.getInstance().time + val difference: Long = now.time - track.recordingStop.time + track.duration = track.duration + difference + track.recordingStop = now + + // add only if recent and accurate + val shouldBeAdded: Boolean + shouldBeAdded = (LocationHelper.isRecentEnough(location) + && LocationHelper.isAccurateEnough(location, locationAccuracyThreshold) + && LocationHelper.isDifferentEnough(previousLocation, location)) + + if (shouldBeAdded) { + // update distance + track.length = track.length + LocationHelper.calculateDistance(previousLocation, location) + + if (location.altitude != 0.0) { + // update altitude values + if (numberOfWayPoints == 0) { + track.maxAltitude = location.altitude + track.minAltitude = location.altitude + } else { + // calculate elevation values + val elevationDifferences: Pair = LocationHelper.calculateElevationDifferences(previousLocation, location, track) + // check if significant differences were calculated + if (elevationDifferences != Pair(track.positiveElevation, track.negativeElevation)) { + // update altitude values + if (location.altitude > track.maxAltitude) track.maxAltitude = location.altitude + if (location.altitude < track.minAltitude) track.minAltitude = location.altitude + // update elevation values + track.positiveElevation = elevationDifferences.first + track.negativeElevation = elevationDifferences.second + } + } + } + + // toggle stop over status, if necessary + if (track.wayPoints.size < 0) { + track.wayPoints[track.wayPoints.size - 1].isStopOver = LocationHelper.isStopOver(previousLocation, location) + } + + // save number of satellites + val numberOfSatellites: Int + val extras = location.extras + if (extras != null && extras.containsKey("satellites")) { + numberOfSatellites = extras.getInt("satellites", 0) + } else { + numberOfSatellites = 0 + } + + // add current location as point to center on for later display + track.latitude = location.latitude + track.longitude = location.longitude + + // add location as new waypoint + track.wayPoints.add(WayPoint(location.provider, location.latitude, location.longitude, location.altitude, location.accuracy, location.time, track.length, numberOfSatellites)) + } + + return track + } + + + /* Creates GPX string for given track */ + fun createGpxString(track: Track): String { + var gpxString: String + + // add header + gpxString = "\n" + + "\n" + + // add track + gpxString += createGpxTrk(track) + + // add closing tag + gpxString += "\n" + + return gpxString + } + + + /* Creates GPX formatted track */ + private fun createGpxTrk(track: Track): String { + val gpxTrack = StringBuilder("") + val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) + dateFormat.timeZone = TimeZone.getTimeZone("UTC") + + // add opening track tag + gpxTrack.append("\t\n") + + // add name to track + gpxTrack.append("\t\t") + gpxTrack.append("Trackbook Recording: ${track.name}") + gpxTrack.append("\n") + + // add opening track segment tag + gpxTrack.append("\t\t\n") + + // add route point + track.wayPoints.forEach { wayPoint -> + // add longitude and latitude + gpxTrack.append("\t\t\t\n") + + // add time + gpxTrack.append("\t\t\t\t\n") + + // add altitude + gpxTrack.append("\t\t\t\t") + gpxTrack.append(wayPoint.altitude) + gpxTrack.append("\n") + + // add closing tag + gpxTrack.append("\t\t\t\n") + } + + // add closing track segment tag + gpxTrack.append("\t\t\n") + + // add closing track tag + gpxTrack.append("\t\n") + + return gpxTrack.toString() + } + + + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/helpers/TrackbookKeys.java b/app/src/main/java/org/y20k/trackbook/helpers/TrackbookKeys.java deleted file mode 100755 index 9a7d902..0000000 --- a/app/src/main/java/org/y20k/trackbook/helpers/TrackbookKeys.java +++ /dev/null @@ -1,122 +0,0 @@ -/** - * TrackbookKeys.java - * Implements the keys used throughout the app - * This interface hosts all keys used to control Trackbook's state - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.helpers; - - -/** - * TrackbookKeys.class - */ -public interface TrackbookKeys { - - /* ACTIONS */ - String ACTION_START = "org.y20k.trackbook.action.START"; - String ACTION_STOP = "org.y20k.trackbook.action.STOP"; - String ACTION_DISMISS = "org.y20k.transistor.action.DISMISS"; - String ACTION_RESUME = "org.y20k.transistor.action.RESUME"; - String ACTION_CLEAR = "org.y20k.transistor.action.CLEAR"; - String ACTION_SAVE = "org.y20k.transistor.action.SAVE"; - String ACTION_DEFAULT = "DEFAULT"; - String ACTION_SHOW_MAP = "SHOW_MAP"; - String ACTION_TRACK_UPDATED = "TRACK_UPDATED"; - String ACTION_TRACK_REQUEST = "TRACK_REQUEST"; - String ACTION_TRACKING_STATE_CHANGED = "TRACKING_STATE_CHANGED"; - String ACTION_TRACK_SAVE = "TRACK_SAVE"; - - /* EXTRAS */ - String EXTRA_TRACK = "TRACK"; - String EXTRA_LAST_LOCATION = "LAST_LOCATION"; - String EXTRA_TRACKING_STATE = "TRACKING_STATE"; - String EXTRA_INFOSHEET_TITLE = "EXTRA_INFOSHEET_TITLE"; - String EXTRA_INFOSHEET_CONTENT = "INFOSHEET_CONTENT"; - String EXTRA_SAVE_FINISHED = "SAVE_FINISHED"; - - /* ARGS */ - String ARG_DIALOG_TITLE = "ArgDialogTitle"; - String ARG_DIALOG_MESSAGE = "ArgDialogMessage"; - String ARG_DIALOG_BUTTON_POSITIVE = "ArgDialogButtonPositive"; - String ARG_DIALOG_BUTTON_NEGATIVE = "ArgDialogButtonNegative"; - - /* PREFS */ - String PREFS_FAB_STATE = "fabStatePrefs"; - String PREFS_TRACKER_SERVICE_RUNNING = "trackerServiceRunning"; - String PREFS_CURRENT_TRACK_DURATION = "currentTrackDuration"; - String PREF_NIGHT_MODE_STATE = "prefNightModeState"; - - /* INSTANCE STATE */ - String INSTANCE_FIRST_START = "firstStart"; - String INSTANCE_TRACKING_STATE = "trackingState"; - String INSTANCE_SELECTED_TAB = "selectedTab"; - String INSTANCE_FAB_SUB_MENU_VISIBLE = "fabSubMenuVisible"; - String INSTANCE_LATITUDE_MAIN_MAP = "latitudeMainMap"; - String INSTANCE_LONGITUDE_MAIN_MAP = "longitudeMainMap"; - String INSTANCE_ZOOM_LEVEL_MAIN_MAP = "zoomLevelMainMap"; - String INSTANCE_TRACK_TRACK_MAP = "trackTrackMap"; - String INSTANCE_LATITUDE_TRACK_MAP = "latitudeTrackMap"; - String INSTANCE_LONGITUDE_TRACK_MAP = "longitudeTrackMap"; - String INSTANCE_ZOOM_LEVEL_TRACK_MAP = "zoomLevelTrackMap"; - String INSTANCE_CURRENT_LOCATION = "currentLocation"; - String INSTANCE_CURRENT_TRACK = "currentTrack"; - - /* FRAGMENT IDS */ - int FRAGMENT_ID_MAP = 0; - int FRAGMENT_ID_TRACKS = 1; - - /* RESULTS */ - int RESULT_SAVE_DIALOG = 1; - int RESULT_CLEAR_DIALOG = 2; - int RESULT_DELETE_DIALOG = 3; - int RESULT_EXPORT_DIALOG = 4; - int RESULT_EMPTY_RECORDING_DIALOG = 5; - - /* CONSTANTS */ - long ONE_SECOND_IN_NANOSECOND = 1000000000L; - long EIGHT_HOURS_IN_MILLISECONDS = 43200000; // maximum tracking duration - long FIFTEEN_SECONDS_IN_MILLISECONDS = 15000; // timer interval for tracking - long FIVE_MINUTES_IN_NANOSECONDS = 5L * 60000000000L; // determines a stop over - long ONE_MINUTE_IN_NANOSECONDS = 1L * 60000000000L; // defines an old location - int MAXIMUM_TRACK_FILES = 25; - int FIFTY_METER_RADIUS = 50; - - /* FILE */ - String FILE_TYPE_GPX_EXTENSION = ".gpx"; - String FILE_TYPE_TRACKBOOK_EXTENSION = ".trackbook"; - String FILE_NAME_TEMP = "temp"; - String TRACKS_DIRECTORY_NAME = "tracks"; - int FILE_TEMP_TRACK = 0; - int FILE_MOST_CURRENT_TRACK = 1; - - /* UNITS */ - int METRIC = 1; - int IMPERIAL = -1; - - /* FLOATING ACTION BUTTON */ - int FAB_STATE_DEFAULT = 0; - int FAB_STATE_RECORDING = 1; - int FAB_STATE_SAVE = 2; - - /* NOTIFICATION */ - int TRACKER_SERVICE_NOTIFICATION_ID = 1; - String NOTIFICATION_CHANEL_ID_RECORDING_CHANNEL ="notificationChannelIdRecordingChannel"; - - /* MISC */ - int CURRENT_TRACK_FORMAT_VERSION = 3; // incremental version number to prevent issues in case the Track format evolves - double DEFAULT_LATITUDE = 71.172500; // latitude Nordkapp, Norway - double DEFAULT_LONGITUDE = 25.784444; // longitude Nordkapp, Norway - int MEASUREMENT_ERROR_THRESHOLD = 10; // altitude changes of 10 meter or more (per 15 seconds) are being discarded - int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124; - -} diff --git a/app/src/main/java/org/y20k/trackbook/helpers/UiHelper.kt b/app/src/main/java/org/y20k/trackbook/helpers/UiHelper.kt new file mode 100644 index 0000000..edad419 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/helpers/UiHelper.kt @@ -0,0 +1,130 @@ +/* + * UiHelper.kt + * Implements the UiHelper object + * A UiHelper provides helper methods for User Interface related tasks + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.escapepod.helpers + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.drawable.ColorDrawable +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import org.y20k.trackbook.R +import org.y20k.trackbook.helpers.LogHelper + + +/* + * UiHelper object + */ +object UiHelper { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(UiHelper::class.java) + + + /* Sets layout margins for given view in DP */ + fun setViewMargins(context: Context, view: View, left: Int = 0, right: Int = 0, top: Int= 0, bottom: Int = 0) { + val scalingFactor: Float = context.resources.displayMetrics.density + val l: Int = (left * scalingFactor).toInt() + val r: Int = (right * scalingFactor).toInt() + val t: Int = (top * scalingFactor).toInt() + val b: Int = (bottom * scalingFactor).toInt() + if (view.layoutParams is ViewGroup.MarginLayoutParams) { + val p = view.layoutParams as ViewGroup.MarginLayoutParams + p.setMargins(l, t, r, b) + view.requestLayout() + } + } + + + /* Sets layout margins for given view in percent */ + fun setViewMarginsPercentage(context: Context, view: View, height: Int, width: Int, left: Int = 0, right: Int = 0, top: Int= 0, bottom: Int = 0) { + val l: Int = ((width / 100.0f) * left).toInt() + val r: Int = ((width / 100.0f) * right).toInt() + val t: Int = ((height / 100.0f) * top).toInt() + val b: Int = ((height / 100.0f) * bottom).toInt() + setViewMargins(context, view, l, r, t, b) + } + + + /* + * Inner class: Callback that detects a left swipe + * Credit: https://github.com/kitek/android-rv-swipe-delete/blob/master/app/src/main/java/pl/kitek/rvswipetodelete/SwipeToDeleteCallback.kt + */ + abstract class SwipeToDeleteCallback(context: Context): ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) { + + private val deleteIcon = ContextCompat.getDrawable(context, R.drawable.ic_remove_circle_24dp) + private val intrinsicWidth: Int = deleteIcon?.intrinsicWidth ?: 0 + private val intrinsicHeight: Int = deleteIcon?.intrinsicHeight ?: 0 + private val background: ColorDrawable = ColorDrawable() + private val backgroundColor = context.resources.getColor(R.color.list_card_delete_background, null) + private val clearPaint: Paint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) } + + override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { + // do nothing + return false + } + + override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) { + val itemView = viewHolder.itemView + val itemHeight = itemView.bottom - itemView.top + val isCanceled = dX == 0f && !isCurrentlyActive + + if (isCanceled) { + clearCanvas(c, itemView.right + dX, itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat()) + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + return + } + + // draw red delete background + background.color = backgroundColor + background.setBounds( + itemView.right + dX.toInt(), + itemView.top, + itemView.right, + itemView.bottom + ) + background.draw(c) + + // calculate position of delete icon + val deleteIconTop = itemView.top + (itemHeight - intrinsicHeight) / 2 + val deleteIconMargin = (itemHeight - intrinsicHeight) / 2 + val deleteIconLeft = itemView.right - deleteIconMargin - intrinsicWidth + val deleteIconRight = itemView.right - deleteIconMargin + val deleteIconBottom = deleteIconTop + intrinsicHeight + + // draw delete icon + deleteIcon?.setBounds(deleteIconLeft, deleteIconTop, deleteIconRight, deleteIconBottom) + deleteIcon?.draw(c) + + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + } + + private fun clearCanvas(c: Canvas?, left: Float, top: Float, right: Float, bottom: Float) { + c?.drawRect(left, top, right, bottom, clearPaint) + } + } + /* + * End of inner class + */ + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/layout/DodgeAbleLayoutBehavior.java b/app/src/main/java/org/y20k/trackbook/layout/DodgeAbleLayoutBehavior.java deleted file mode 100755 index e1eb7bc..0000000 --- a/app/src/main/java/org/y20k/trackbook/layout/DodgeAbleLayoutBehavior.java +++ /dev/null @@ -1,60 +0,0 @@ -/** - * DodgeAbleLayoutBehavior.java - * Implements the DodgeAbleLayoutBehavior class - * A DodgeAbleLayoutBehavior enables any element to be dodged up by a snackbar - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - - -package org.y20k.trackbook.layout; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; - -import com.google.android.material.snackbar.Snackbar; - -import androidx.coordinatorlayout.widget.CoordinatorLayout; - - -/** - * DodgeAbleLayoutBehavior class - * adapted from: http://stackoverflow.com/a/35904421 - */ -public class DodgeAbleLayoutBehavior extends CoordinatorLayout.Behavior { - - /* Constructor (default) */ - public DodgeAbleLayoutBehavior() { - super(); - } - - - /* Constructor for context and attributes */ - public DodgeAbleLayoutBehavior(Context context, AttributeSet attrs) { - super(context, attrs); - } - - - @Override - public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { - return dependency instanceof Snackbar.SnackbarLayout; - } - - - @Override - public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) { - float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight()); - child.setTranslationY(translationY); - return true; - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/layout/NonSwipeableViewPager.java b/app/src/main/java/org/y20k/trackbook/layout/NonSwipeableViewPager.java deleted file mode 100755 index 842d1b0..0000000 --- a/app/src/main/java/org/y20k/trackbook/layout/NonSwipeableViewPager.java +++ /dev/null @@ -1,102 +0,0 @@ -/** - * NonSwipeableViewPager.java - * Implements the NonSwipeableViewPager class - * A NonSwipeableViewPager is a ViewPager with swiping gestures disabled - * - * This file is part of - * TRACKBOOK - Movement Recorder for Android - * - * Copyright (c) 2016-19 - Y20K.org - * Licensed under the MIT-License - * http://opensource.org/licenses/MIT - * - * Trackbook uses osmdroid - OpenStreetMap-Tools for Android - * https://github.com/osmdroid/osmdroid - */ - -package org.y20k.trackbook.layout; - -/** - * NonSwipeableViewPager class - * adapted from: http://stackoverflow.com/a/9650884 - */ - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.animation.DecelerateInterpolator; -import android.widget.Scroller; - -import org.y20k.trackbook.helpers.LogHelper; - -import java.lang.reflect.Field; - -import androidx.viewpager.widget.ViewPager; - - -public class NonSwipeableViewPager extends ViewPager { - - /* Define log tag */ - private static final String LOG_TAG = NonSwipeableViewPager.class.getSimpleName(); - - - /* Constructor */ - public NonSwipeableViewPager(Context context) { - super(context); - setMyScroller(); - } - - - /* Constructor */ - public NonSwipeableViewPager(Context context, AttributeSet attrs) { - super(context, attrs); - setMyScroller(); - } - - - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - // Never allow swiping to switch between pages - return false; - } - - - @Override - public boolean onTouchEvent(MotionEvent event) { - // Never allow swiping to switch between pages - return false; - } - - - /* Attaches a custom smooth scrolling scroller to a ViewPager */ - private void setMyScroller() { - try { - Class viewpager = ViewPager.class; - Field scroller = viewpager.getDeclaredField("mScroller"); - scroller.setAccessible(true); - scroller.set(this, new MyScroller(getContext())); - } catch (Exception e) { - LogHelper.e(LOG_TAG, "Problem accessing or modifying the mScroller field. Exception: " + e); - e.printStackTrace(); - } - } - - - /** - * Inner class: MyScroller is a custom Scroller - */ - public class MyScroller extends Scroller { - public MyScroller(Context context) { - super(context, new DecelerateInterpolator()); - } - - @Override - public void startScroll(int startX, int startY, int dx, int dy, int duration) { - super.startScroll(startX, startY, dx, dy, 350 /*1 secs*/); - } - } - /** - * End of inner class - */ - -} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/tracklist/TracklistAdapter.kt b/app/src/main/java/org/y20k/trackbook/tracklist/TracklistAdapter.kt new file mode 100644 index 0000000..98feb30 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/tracklist/TracklistAdapter.kt @@ -0,0 +1,208 @@ +/* + * TracklistAdapter.kt + * Implements the TracklistAdapter class + * A TracklistAdapter is a custom adapter for a RecyclerView + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.tracklist + + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.* +import org.y20k.trackbook.R +import org.y20k.trackbook.core.Tracklist +import org.y20k.trackbook.core.TracklistElement +import org.y20k.trackbook.helpers.* +import java.util.* + + +/* + * TracklistAdapter class + */ +class TracklistAdapter(private val fragment: Fragment) : RecyclerView.Adapter() { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(TracklistAdapter::class.java) + + + /* Main class variables */ + private val context: Context = fragment.activity as Context + private lateinit var tracklistListener: TracklistAdapterListener + private var useImperial: Boolean = PreferencesHelper.loadUseImperialUnits(context) + private var tracklist: Tracklist = Tracklist() + + + /* Listener Interface */ + interface TracklistAdapterListener { + fun onTrackElementTapped(tracklistElement: TracklistElement) { } + // fun onTrackElementStarred(trackId: Long, starred: Boolean) + } + + + /* Overrides onAttachedToRecyclerView from RecyclerView.Adapter */ + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + // get reference to listener + tracklistListener = fragment as TracklistAdapterListener + // load tracklist + tracklist = FileHelper.readTracklist(context) + tracklist.tracklistElements.sortByDescending { tracklistElement -> tracklistElement.date } + } + + + /* Overrides onCreateViewHolder from RecyclerView.Adapter */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val v = LayoutInflater.from(parent.context).inflate(R.layout.track_element, parent, false) + return TrackElementViewHolder(v) + } + + + /* Overrides getItemCount from RecyclerView.Adapter */ + override fun getItemCount(): Int { + return tracklist.tracklistElements.size + } + + + /* Overrides onBindViewHolder from RecyclerView.Adapter */ + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val trackElementViewHolder: TrackElementViewHolder = holder as TrackElementViewHolder + trackElementViewHolder.trackNameView.text = tracklist.tracklistElements[position].name + trackElementViewHolder.trackDataView.text = createTrackDataString(position) + when (tracklist.tracklistElements[position].starred) { + true -> trackElementViewHolder.starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_24dp)) + false -> trackElementViewHolder.starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_border_24dp)) + } + trackElementViewHolder.trackElement.setOnClickListener { + tracklistListener.onTrackElementTapped(tracklist.tracklistElements[position]) + } + trackElementViewHolder.starButton.setOnClickListener { + toggleStarred(it, position) + } + } + + + /* Get track name for given position */ + fun getTrackName(position: Int): String { + return tracklist.tracklistElements[position].name + } + + + /* Removes track and track files for given position - used by TracklistFragment */ + fun removeTrack(context: Context, position: Int) { + val backgroundJob = Job() + val uiScope = CoroutineScope(Dispatchers.Main + backgroundJob) + uiScope.launch { + notifyItemRemoved(position) + val deferred: Deferred = async { FileHelper.deleteTrackSuspended(context, position, tracklist) } + // wait for result and store in tracklist + tracklist = deferred.await() + backgroundJob.cancel() + } + } + + + /* Finds current position of track element in adapter list */ + fun findPosition(trackId: Long): Int { + tracklist.tracklistElements.forEachIndexed {index, tracklistElement -> + if (tracklistElement.getTrackId() == trackId) return index + } + return -1 + } + + + /* Toggles the starred state of tracklist element - and saves tracklist */ + private fun toggleStarred(view: View, position: Int) { + val starButton: ImageButton = view as ImageButton + when (tracklist.tracklistElements[position].starred) { + true -> { + starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_border_24dp)) + tracklist.tracklistElements[position].starred = false + } + false -> { + starButton.setImageDrawable(context.getDrawable(R.drawable.ic_star_24dp)) + tracklist.tracklistElements[position].starred = true + } + } + GlobalScope.launch { + FileHelper.saveTracklistSuspended(context, tracklist, GregorianCalendar.getInstance().time) + } + } + + + /* Creates the track data string */ + private fun createTrackDataString(position: Int): String { + val tracklistElement: TracklistElement = tracklist.tracklistElements[position] + val trackDataString: String + when (tracklistElement.name == tracklistElement.dateString) { + // CASE: no individual name set - exclude date + true -> trackDataString = "${LengthUnitHelper.convertDistanceToString(tracklistElement.length, useImperial)} • ${tracklistElement.durationString}" + // CASE: no individual name set - include date + false -> trackDataString = "${tracklistElement.dateString} • ${LengthUnitHelper.convertDistanceToString(tracklistElement.length, useImperial)} • ${tracklistElement.durationString}" + } + return trackDataString + } + + + /* + * Inner class: DiffUtil.Callback that determines changes in data - improves list performance + */ + private inner class DiffCallback(val oldList: Tracklist, val newList: Tracklist): DiffUtil.Callback() { + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList.tracklistElements[oldItemPosition] + val newItem = newList.tracklistElements[newItemPosition] + return TrackHelper.getTrackId(oldItem) == TrackHelper.getTrackId(newItem) + } + + override fun getOldListSize(): Int { + return oldList.tracklistElements.size + } + + override fun getNewListSize(): Int { + return newList.tracklistElements.size + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList.tracklistElements[oldItemPosition] + val newItem = newList.tracklistElements[newItemPosition] + return TrackHelper.getTrackId(oldItem) == TrackHelper.getTrackId(newItem) && oldItem.length == newItem.length + } + } + /* + * End of inner class + */ + + + /* + * Inner class: ViewHolder for a track element + */ + private inner class TrackElementViewHolder (trackElementLayout: View): RecyclerView.ViewHolder(trackElementLayout) { + val trackElement: ConstraintLayout = trackElementLayout.findViewById(R.id.track_element) + val trackNameView: TextView = trackElementLayout.findViewById(R.id.track_name) + val trackDataView: TextView = trackElementLayout.findViewById(R.id.track_data) + val starButton: ImageButton = trackElementLayout.findViewById(R.id.star_button) + } + /* + * End of inner class + */ + +} diff --git a/app/src/main/java/org/y20k/trackbook/ui/MapFragmentLayoutHolder.kt b/app/src/main/java/org/y20k/trackbook/ui/MapFragmentLayoutHolder.kt new file mode 100644 index 0000000..bb65884 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/ui/MapFragmentLayoutHolder.kt @@ -0,0 +1,233 @@ +/* + * MapFragmentLayoutHolder.kt + * Implements the MapFragmentLayoutHolder class + * A MapFragmentLayoutHolder hold references to the main views of a map fragment + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.ui + +import android.Manifest +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.Group +import androidx.core.content.ContextCompat +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.snackbar.Snackbar +import org.osmdroid.api.IMapController +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.MapView +import org.osmdroid.views.overlay.ItemizedIconOverlay +import org.osmdroid.views.overlay.OverlayItem +import org.osmdroid.views.overlay.TilesOverlay +import org.osmdroid.views.overlay.compass.CompassOverlay +import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider +import org.y20k.trackbook.Keys +import org.y20k.trackbook.R +import org.y20k.trackbook.core.Track +import org.y20k.trackbook.helpers.LogHelper +import org.y20k.trackbook.helpers.MapHelper +import org.y20k.trackbook.helpers.NightModeHelper +import org.y20k.trackbook.helpers.PreferencesHelper + + +/* + * MapFragmentLayoutHolder class + */ +data class MapFragmentLayoutHolder(var context: Context, var inflater: LayoutInflater, var container: ViewGroup?, val startLocation: Location, val trackingState: Int) { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(MapFragmentLayoutHolder::class.java) + + + /* Main class variables */ + val rootView: View + val mapView: MapView + val currentLocationButton: FloatingActionButton + val recordingButton: FloatingActionButton + val recordingButtonSubMenu: Group + val saveButton: FloatingActionButton + val clearButton: FloatingActionButton + val resumeButton: FloatingActionButton + var userInteraction: Boolean = false + private var currentPositionOverlay: ItemizedIconOverlay + private var currentTrackOverlay: ItemizedIconOverlay? + private var locationErrorBar: Snackbar + private var controller: IMapController + private var zoomLevel: Double + + + /* Init block */ + init { + // find views + rootView = inflater.inflate(R.layout.fragment_map, container, false) + mapView = rootView.findViewById(R.id.map) + currentLocationButton = rootView.findViewById(R.id.fab_location_button) + recordingButton = rootView.findViewById(R.id.fab_main_button) + recordingButtonSubMenu = rootView.findViewById(R.id.fab_sub_menu) + saveButton = rootView.findViewById(R.id.fab_sub_menu_button_save) + clearButton = rootView.findViewById(R.id.fab_sub_menu_button_clear) + resumeButton = rootView.findViewById(R.id.fab_sub_menu_button_resume) + locationErrorBar = Snackbar.make(mapView, String(), Snackbar.LENGTH_INDEFINITE) + + // basic map setup + controller = mapView.controller + mapView.isTilesScaledToDpi = true + mapView.setTileSource(TileSourceFactory.MAPNIK) + mapView.setMultiTouchControls(true) + mapView.zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER) + zoomLevel = PreferencesHelper.loadZoomLevel(context) + controller.setZoom(zoomLevel) + + // set dark map tiles, if necessary + if (NightModeHelper.isNightModeOn(context as Activity)) { + mapView.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS) + } + + // add compass to map + val compassOverlay = CompassOverlay(context, InternalCompassOrientationProvider(context), mapView) + compassOverlay.enableCompass() + compassOverlay.setCompassCenter(36f, 60f) + + mapView.overlays.add(compassOverlay) + + // add my location overlay + currentPositionOverlay = MapHelper.createMyLocationOverlay(context, startLocation, trackingState) + mapView.overlays.add(currentPositionOverlay) + centerMap(startLocation) + + // initialize track overlay + currentTrackOverlay = null + + // initialize recording button state + updateRecordingButton(trackingState) + + // add touch listeners + addTouchListeners() + + // listen for user interaction + addInteractionListener() + } + + + /* Listen for user interaction */ + @SuppressLint("ClickableViewAccessibility") + private fun addInteractionListener() { + mapView.setOnTouchListener { v, event -> + userInteraction = true + false + } + } + + + /* Set map center */ + fun centerMap(location: Location, animated: Boolean = false) { + val position = GeoPoint(location.latitude, location.longitude) + when (animated) { + true -> controller.animateTo(position) + false -> controller.setCenter(position) + } + userInteraction = false + } + + + /* Save current best location and state of map to shared preferences */ + fun saveState(currentBestLocation: Location) { + PreferencesHelper.saveCurrentBestLocation(context, currentBestLocation) + PreferencesHelper.saveZoomLevel(context, mapView.getZoomLevelDouble()) + // reset user interaction state + userInteraction = false + } + + + /* Mark current position on map */ + fun markCurrentPosition(location: Location, trackingState: Int = Keys.STATE_NOT_TRACKING) { + mapView.overlays.remove(currentPositionOverlay) + currentPositionOverlay = MapHelper.createMyLocationOverlay(context, location, trackingState) + mapView.overlays.add(currentPositionOverlay) + } + + + /* Overlay current track on map */ + fun overlayCurrentTrack(track: Track, trackingState: Int) { + if (currentTrackOverlay != null) { + mapView.overlays.remove(currentTrackOverlay) + } + if (track.wayPoints.isNotEmpty()) { + currentTrackOverlay = MapHelper.createTrackOverlay(context, track, trackingState) + mapView.overlays.add(currentTrackOverlay) + } + } + + + /* Toggles state of recording button and sub menu_bottom_navigation */ + fun updateRecordingButton(trackingState: Int) { + when (trackingState) { + Keys.STATE_NOT_TRACKING -> { + recordingButton.setImageResource(R.drawable.ic_fiber_manual_record_white_24dp) + recordingButtonSubMenu.visibility = View.GONE + } + Keys.STATE_TRACKING_ACTIVE -> { + recordingButton.setImageResource(R.drawable.ic_fiber_manual_record_red_24dp) + recordingButtonSubMenu.visibility = View.GONE + } + Keys.STATE_TRACKING_STOPPED -> { + recordingButton.setImageResource(R.drawable.ic_save_white_24dp) + } + } + } + + + /* Toggles visibility of recording button sub menu_bottom_navigation */ + fun toggleRecordingButtonSubMenu() { + when (recordingButtonSubMenu.visibility) { + View.VISIBLE -> recordingButtonSubMenu.visibility = View.GONE + else -> recordingButtonSubMenu.visibility = View.VISIBLE + } + } + + + + /* Toggles content and visibility of the location error snackbar */ + fun toggleLocationErrorBar(gpsProviderActive: Boolean, networkProviderActive: Boolean) { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) { + // CASE: Location permission not granted + locationErrorBar.setText(R.string.snackbar_message_location_permission_denied) + locationErrorBar.show() + } else if (!gpsProviderActive && !networkProviderActive) { + // CASE: Location setting is off + locationErrorBar.setText(R.string.snackbar_message_location_offline) + locationErrorBar.show() + } else if (locationErrorBar.isShown) { + // CASE: Snackbar is visible but unnecessary + locationErrorBar.dismiss() + } + } + + + /* Sets up views - adds touch listeners */ + private fun addTouchListeners() { + currentLocationButton.setOnLongClickListener { + NightModeHelper.switchMode(context as Activity) + return@setOnLongClickListener true + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/y20k/trackbook/ui/TrackFragmentLayoutHolder.kt b/app/src/main/java/org/y20k/trackbook/ui/TrackFragmentLayoutHolder.kt new file mode 100644 index 0000000..1c6fef5 --- /dev/null +++ b/app/src/main/java/org/y20k/trackbook/ui/TrackFragmentLayoutHolder.kt @@ -0,0 +1,254 @@ +/* + * TrackFragmentLayoutHolder.kt + * Implements the TrackFragmentLayoutHolder class + * A TrackFragmentLayoutHolder hold references to the main views of a track fragment + * + * This file is part of + * TRACKBOOK - Movement Recorder for Android + * + * Copyright (c) 2016-20 - Y20K.org + * Licensed under the MIT-License + * http://opensource.org/licenses/MIT + * + * Trackbook uses osmdroid - OpenStreetMap-Tools for Android + * https://github.com/osmdroid/osmdroid + */ + + +package org.y20k.trackbook.ui + +import android.app.Activity +import android.content.Context +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.Toast +import androidx.constraintlayout.widget.Group +import androidx.core.widget.NestedScrollView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.textview.MaterialTextView +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.osmdroid.api.IGeoPoint +import org.osmdroid.api.IMapController +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.MapView +import org.osmdroid.views.overlay.ItemizedIconOverlay +import org.osmdroid.views.overlay.OverlayItem +import org.osmdroid.views.overlay.TilesOverlay +import org.osmdroid.views.overlay.compass.CompassOverlay +import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider +import org.y20k.trackbook.Keys +import org.y20k.trackbook.R +import org.y20k.trackbook.core.Track +import org.y20k.trackbook.helpers.* +import kotlin.math.roundToInt + + +/* + * TrackFragmentLayoutHolder class + */ +data class TrackFragmentLayoutHolder(var context: Context, var inflater: LayoutInflater, var container: ViewGroup?, var arguments: Bundle?) { + + /* Define log tag */ + private val TAG: String = LogHelper.makeLogTag(TrackFragmentLayoutHolder::class.java) + + + /* Main class variables */ + val rootView: View + val track: Track + val shareButton: ImageButton + val deleteButton: ImageButton + val editButton: ImageButton + val trackNameView: MaterialTextView + private val mapView: MapView + private var trackOverlay: ItemizedIconOverlay? + private var controller: IMapController + private var zoomLevel: Double + private val statisticsSheetBehavior: BottomSheetBehavior + private val statisticsSheet: NestedScrollView + private val statisticsView: View + private val distanceView: MaterialTextView + private val stepsView: MaterialTextView + private val waypointsView: MaterialTextView + private val durationView: MaterialTextView + private val recordingStartView: MaterialTextView + private val recordingStopView: MaterialTextView + private val maxAltitudeView: MaterialTextView + private val minAltitudeView: MaterialTextView + private val positiveElevationView: MaterialTextView + private val negativeElevationView: MaterialTextView + private val elevationDataViews: Group + private val trackManagementViews: Group + private val useImperialUnits: Boolean + + + /* Init block */ + init { + // find views + rootView = inflater.inflate(R.layout.fragment_track, container, false) + mapView = rootView.findViewById(R.id.map) + shareButton = rootView.findViewById(R.id.share_button) + deleteButton = rootView.findViewById(R.id.delete_button) + editButton = rootView.findViewById(R.id.edit_button) + trackNameView = rootView.findViewById(R.id.statistics_track_name_headline) + + // basic map setup + controller = mapView.controller + mapView.isTilesScaledToDpi = true + mapView.setTileSource(TileSourceFactory.MAPNIK) + mapView.setMultiTouchControls(true) + mapView.zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER) + zoomLevel = Keys.DEFAULT_ZOOM_LEVEL + controller.setZoom(zoomLevel) + + // get views for statistics sheet + statisticsSheet = rootView.findViewById(R.id.statistics_sheet) + statisticsView = rootView.findViewById(R.id.statistics_view) + distanceView = rootView.findViewById(R.id.statistics_data_distance) + stepsView = rootView.findViewById(R.id.statistics_data_steps) + waypointsView = rootView.findViewById(R.id.statistics_data_waypoints) + durationView = rootView.findViewById(R.id.statistics_data_duration) + recordingStartView = rootView.findViewById(R.id.statistics_data_recording_start) + recordingStopView = rootView.findViewById(R.id.statistics_data_recording_stop) + maxAltitudeView = rootView.findViewById(R.id.statistics_data_max_altitude) + minAltitudeView = rootView.findViewById(R.id.statistics_data_min_altitude) + positiveElevationView = rootView.findViewById(R.id.statistics_data_positive_elevation) + negativeElevationView = rootView.findViewById(R.id.statistics_data_negative_elevation) + elevationDataViews = rootView.findViewById(R.id.elevation_data) + trackManagementViews = rootView.findViewById(R.id.management_icons) + + // get measurement unit system + useImperialUnits = PreferencesHelper.loadUseImperialUnits(context) + + // set dark map tiles, if necessary + if (NightModeHelper.isNightModeOn(context as Activity)) { + mapView.getOverlayManager().getTilesOverlay().setColorFilter(TilesOverlay.INVERT_COLORS) + } + + // add compass to map + val compassOverlay = CompassOverlay(context, InternalCompassOrientationProvider(context), mapView) + compassOverlay.enableCompass() + compassOverlay.setCompassCenter(36f, 60f) + mapView.overlays.add(compassOverlay) + + // get track and create map overlay + val fileUriString: String = arguments?.getString(Keys.ARG_TRACK_FILE_URI, String()) ?: String() + if (fileUriString.isNotBlank()) { + track = FileHelper.readTrack(context, Uri.parse(fileUriString)) + } else { + track = Track() + } + trackOverlay = MapHelper.createTrackOverlay(context, track, Keys.STATE_NOT_TRACKING) + if (track.wayPoints.isNotEmpty()) { + mapView.overlays.add(trackOverlay) + } + + // set up and show statistics sheet + statisticsSheetBehavior = BottomSheetBehavior.from(statisticsSheet) + statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + statisticsSheetBehavior.addBottomSheetCallback(getStatisticsSheetCallback()) + setupStatisticsViews() + } + + + /* Updates zoom level and center of this map */ + fun updateMapView() { + val position = GeoPoint(track.latitude, track.longitude) + controller.setCenter(position) + controller.setZoom(track.zoomLevel) + } + + + /* Saves zoom level and center of this map */ + fun saveViewStateToTrack() { + val center: IGeoPoint = mapView.mapCenter + track.latitude = center.latitude + track.longitude = center.longitude + track.zoomLevel = mapView.zoomLevelDouble + GlobalScope.launch { FileHelper.saveTrackSuspended(track, false) } + } + + + /* Sets up the statistics sheet */ + private fun setupStatisticsViews() { + + // get step count string + val steps: String + if (track.stepCount == -1f) steps = context.getString(R.string.statistics_sheet_p_steps_no_pedometer) + else steps = track.stepCount.roundToInt().toString() + + // populate views + trackNameView.text = track.name + distanceView.text = LengthUnitHelper.convertDistanceToString(track.length, useImperialUnits) + stepsView.text = steps + waypointsView.text = track.wayPoints.size.toString() + durationView.text = DateTimeHelper.convertToReadableTime(context, track.duration) + recordingStartView.text = DateTimeHelper.convertToReadableDate(track.recordingStart) + recordingStopView.text = DateTimeHelper.convertToReadableDate(track.recordingStart) + maxAltitudeView.text = LengthUnitHelper.convertDistanceToString(track.maxAltitude, useImperialUnits) + minAltitudeView.text = LengthUnitHelper.convertDistanceToString(track.minAltitude, useImperialUnits) + positiveElevationView.text = LengthUnitHelper.convertDistanceToString(track.positiveElevation, useImperialUnits) + negativeElevationView.text = LengthUnitHelper.convertDistanceToString(track.negativeElevation, useImperialUnits) + + // inform user about possible accuracy issues with altitude measurements + elevationDataViews.referencedIds.forEach { id -> + (rootView.findViewById(id) as View).setOnClickListener{ + Toast.makeText(context, R.string.toast_message_elevation_info, Toast.LENGTH_LONG).show() + } + } + // make track name on statistics sheet clickable + trackNameView.setOnClickListener { + toggleStatisticsSheetVisibility() + } + } + + + /* Shows/hides the statistics sheet */ + private fun toggleStatisticsSheetVisibility() { + when (statisticsSheetBehavior.state) { + BottomSheetBehavior.STATE_EXPANDED -> statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + else -> statisticsSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } + + + /* Defines the behavior of the statistics sheet */ + private fun getStatisticsSheetCallback(): BottomSheetBehavior.BottomSheetCallback { + return object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + when (newState) { + BottomSheetBehavior.STATE_EXPANDED -> { + statisticsSheet.background = context.getDrawable(R.drawable.shape_statistics_background_expanded) + trackManagementViews.visibility = View.VISIBLE + shareButton.visibility = View.GONE + // bottomSheet.setPadding(0,24,0,0) + } + else -> { + statisticsSheet.background = context.getDrawable(R.drawable.shape_statistics_background_collapsed) + trackManagementViews.visibility = View.GONE + shareButton.visibility = View.VISIBLE + // bottomSheet.setPadding(0,0,0,0) + } + } + } + override fun onSlide(bottomSheet: View, slideOffset: Float) { + if (slideOffset < 0.125f) { + statisticsSheet.background = context.getDrawable(R.drawable.shape_statistics_background_collapsed) + trackManagementViews.visibility = View.GONE + shareButton.visibility = View.VISIBLE + } else { + statisticsSheet.background = context.getDrawable(R.drawable.shape_statistics_background_expanded) + trackManagementViews.visibility = View.VISIBLE + shareButton.visibility = View.GONE + } + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..6348baa --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_account_circle_black.xml b/app/src/main/res/drawable/ic_account_circle_black.xml new file mode 100644 index 0000000..7de6442 --- /dev/null +++ b/app/src/main/res/drawable/ic_account_circle_black.xml @@ -0,0 +1,8 @@ + + + diff --git a/app/src/main/res/drawable/ic_compass_needle_black_24dp.xml b/app/src/main/res/drawable/ic_compass_needle_black_24dp.xml index fbd8e47..df07973 100755 --- a/app/src/main/res/drawable/ic_compass_needle_black_24dp.xml +++ b/app/src/main/res/drawable/ic_compass_needle_black_24dp.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_my_location_24dp.xml b/app/src/main/res/drawable/ic_current_location_24dp.xml similarity index 90% rename from app/src/main/res/drawable/ic_my_location_24dp.xml rename to app/src/main/res/drawable/ic_current_location_24dp.xml index 609c58d..46e57f3 100755 --- a/app/src/main/res/drawable/ic_my_location_24dp.xml +++ b/app/src/main/res/drawable/ic_current_location_24dp.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_delete_forever_24dp.xml b/app/src/main/res/drawable/ic_delete_24dp.xml similarity index 88% rename from app/src/main/res/drawable/ic_delete_forever_24dp.xml rename to app/src/main/res/drawable/ic_delete_24dp.xml index d61792d..651f304 100755 --- a/app/src/main/res/drawable/ic_delete_forever_24dp.xml +++ b/app/src/main/res/drawable/ic_delete_24dp.xml @@ -5,5 +5,5 @@ android:viewportHeight="24.0"> + android:fillColor="@color/icon_default" /> diff --git a/app/src/main/res/drawable/ic_edit_24dp.xml b/app/src/main/res/drawable/ic_edit_24dp.xml new file mode 100644 index 0000000..1a9a294 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_email_black_24dp.xml b/app/src/main/res/drawable/ic_email_black_24dp.xml new file mode 100644 index 0000000..ce97ab8 --- /dev/null +++ b/app/src/main/res/drawable/ic_email_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_info_24dp.xml b/app/src/main/res/drawable/ic_info_24dp.xml index b701d0e..0da35e2 100755 --- a/app/src/main/res/drawable/ic_info_24dp.xml +++ b/app/src/main/res/drawable/ic_info_24dp.xml @@ -5,5 +5,5 @@ android:viewportHeight="24.0"> + android:fillColor="@color/icon_default" /> diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..a0ad202 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_my_location_dot_blue_24dp.xml b/app/src/main/res/drawable/ic_marker_location_blue_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_dot_blue_24dp.xml rename to app/src/main/res/drawable/ic_marker_location_blue_24dp.xml diff --git a/app/src/main/res/drawable/ic_my_location_dot_blue_grey_24dp.xml b/app/src/main/res/drawable/ic_marker_location_blue_grey_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_dot_blue_grey_24dp.xml rename to app/src/main/res/drawable/ic_marker_location_blue_grey_24dp.xml diff --git a/app/src/main/res/drawable/ic_my_location_dot_red_24dp.xml b/app/src/main/res/drawable/ic_marker_location_red_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_dot_red_24dp.xml rename to app/src/main/res/drawable/ic_marker_location_red_24dp.xml diff --git a/app/src/main/res/drawable/ic_my_location_dot_red_grey_24dp.xml b/app/src/main/res/drawable/ic_marker_location_red_grey_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_dot_red_grey_24dp.xml rename to app/src/main/res/drawable/ic_marker_location_red_grey_24dp.xml diff --git a/app/src/main/res/drawable/ic_my_location_crumb_blue_24dp.xml b/app/src/main/res/drawable/ic_marker_track_location_blue_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_crumb_blue_24dp.xml rename to app/src/main/res/drawable/ic_marker_track_location_blue_24dp.xml diff --git a/app/src/main/res/drawable/ic_my_location_crumb_grey_24dp.xml b/app/src/main/res/drawable/ic_marker_track_location_grey_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_crumb_grey_24dp.xml rename to app/src/main/res/drawable/ic_marker_track_location_grey_24dp.xml diff --git a/app/src/main/res/drawable/ic_my_location_crumb_red_24dp.xml b/app/src/main/res/drawable/ic_marker_track_location_red_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_crumb_red_24dp.xml rename to app/src/main/res/drawable/ic_marker_track_location_red_24dp.xml diff --git a/app/src/main/res/drawable/ic_my_location_crumb_transparent_24dp.xml b/app/src/main/res/drawable/ic_marker_track_location_transparent_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_my_location_crumb_transparent_24dp.xml rename to app/src/main/res/drawable/ic_marker_track_location_transparent_24dp.xml diff --git a/app/src/main/res/drawable/ic_notification_action_resume_36dp.xml b/app/src/main/res/drawable/ic_notification_action_resume_36dp.xml new file mode 100644 index 0000000..f1017e0 --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_action_resume_36dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notification_action_show_36dp.xml b/app/src/main/res/drawable/ic_notification_action_show_36dp.xml new file mode 100644 index 0000000..4c1ec9c --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_action_show_36dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_notification_action_stop_24dp.xml b/app/src/main/res/drawable/ic_notification_action_stop_24dp.xml new file mode 100644 index 0000000..cecebba --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_action_stop_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_notification_large_tracking_48dp.xml b/app/src/main/res/drawable/ic_notification_icon_large_tracking_active_48dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_notification_large_tracking_48dp.xml rename to app/src/main/res/drawable/ic_notification_icon_large_tracking_active_48dp.xml diff --git a/app/src/main/res/drawable/ic_notification_large_not_tracking_48dp.xml b/app/src/main/res/drawable/ic_notification_icon_large_tracking_stopped_48dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_notification_large_not_tracking_48dp.xml rename to app/src/main/res/drawable/ic_notification_icon_large_tracking_stopped_48dp.xml diff --git a/app/src/main/res/drawable/ic_notification_small_24dp.xml b/app/src/main/res/drawable/ic_notification_icon_small_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_notification_small_24dp.xml rename to app/src/main/res/drawable/ic_notification_icon_small_24dp.xml diff --git a/app/src/main/res/drawable/ic_notifications_black.xml b/app/src/main/res/drawable/ic_notifications_black.xml new file mode 100644 index 0000000..41d88a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_person_black_24dp.xml b/app/src/main/res/drawable/ic_person_black_24dp.xml new file mode 100644 index 0000000..b2cb337b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_person_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_question_answer_black_24dp.xml b/app/src/main/res/drawable/ic_question_answer_black_24dp.xml new file mode 100644 index 0000000..26eda09 --- /dev/null +++ b/app/src/main/res/drawable/ic_question_answer_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_remove_circle_24dp.xml b/app/src/main/res/drawable/ic_remove_circle_24dp.xml new file mode 100644 index 0000000..31439c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_remove_circle_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_black.xml b/app/src/main/res/drawable/ic_settings_black.xml new file mode 100644 index 0000000..c44b9ff --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_black.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_black_24dp.xml b/app/src/main/res/drawable/ic_settings_black_24dp.xml new file mode 100644 index 0000000..48597cc --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_share_24dp.xml b/app/src/main/res/drawable/ic_share_24dp.xml index c19e95c..24bf669 100755 --- a/app/src/main/res/drawable/ic_share_24dp.xml +++ b/app/src/main/res/drawable/ic_share_24dp.xml @@ -5,5 +5,5 @@ android:viewportHeight="24.0"> + android:fillColor="@color/icon_default" /> diff --git a/app/src/main/res/drawable/ic_star_24dp.xml b/app/src/main/res/drawable/ic_star_24dp.xml new file mode 100644 index 0000000..5e1387f --- /dev/null +++ b/app/src/main/res/drawable/ic_star_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_border_24dp.xml b/app/src/main/res/drawable/ic_star_border_24dp.xml new file mode 100644 index 0000000..81babd1 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_border_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_bottom_navigation.xml b/app/src/main/res/drawable/selector_bottom_navigation.xml index dfc4cc0..1c4350c 100755 --- a/app/src/main/res/drawable/selector_bottom_navigation.xml +++ b/app/src/main/res/drawable/selector_bottom_navigation.xml @@ -1,6 +1,5 @@ - - + diff --git a/app/src/main/res/drawable/shape_statistics_background_collapsed.xml b/app/src/main/res/drawable/shape_statistics_background_collapsed.xml new file mode 100644 index 0000000..3f75958 --- /dev/null +++ b/app/src/main/res/drawable/shape_statistics_background_collapsed.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/shape_statistics_background_expanded.xml b/app/src/main/res/drawable/shape_statistics_background_expanded.xml new file mode 100644 index 0000000..682b9e8 --- /dev/null +++ b/app/src/main/res/drawable/shape_statistics_background_expanded.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml old mode 100755 new mode 100644 index b4f0a6c..39039c1 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,220 +1,34 @@ - + tools:context=".MainActivity"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="0dp" + app:defaultNavHost="true" + app:layout_constraintBottom_toTopOf="@id/bottom_navigation_view" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:navGraph="@navigation/nav_graph_main" /> + + diff --git a/app/src/main/res/layout/custom_dropdown_item_collapsed.xml b/app/src/main/res/layout/custom_dropdown_item_collapsed.xml deleted file mode 100755 index 214bdca..0000000 --- a/app/src/main/res/layout/custom_dropdown_item_collapsed.xml +++ /dev/null @@ -1,13 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/custom_dropdown_item_expanded.xml b/app/src/main/res/layout/custom_dropdown_item_expanded.xml deleted file mode 100755 index 214bdca..0000000 --- a/app/src/main/res/layout/custom_dropdown_item_expanded.xml +++ /dev/null @@ -1,13 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_generic_with_details.xml b/app/src/main/res/layout/dialog_generic_with_details.xml new file mode 100644 index 0000000..89af16d --- /dev/null +++ b/app/src/main/res/layout/dialog_generic_with_details.xml @@ -0,0 +1,57 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_rename_track.xml b/app/src/main/res/layout/dialog_rename_track.xml new file mode 100644 index 0000000..69058e5 --- /dev/null +++ b/app/src/main/res/layout/dialog_rename_track.xml @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main_map.xml b/app/src/main/res/layout/fragment_main_map.xml deleted file mode 100755 index ef50963..0000000 --- a/app/src/main/res/layout/fragment_main_map.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml new file mode 100644 index 0000000..60e023b --- /dev/null +++ b/app/src/main/res/layout/fragment_map.xml @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_main_track.xml b/app/src/main/res/layout/fragment_track.xml similarity index 54% rename from app/src/main/res/layout/fragment_main_track.xml rename to app/src/main/res/layout/fragment_track.xml index 9da09f3..5611711 100755 --- a/app/src/main/res/layout/fragment_main_track.xml +++ b/app/src/main/res/layout/fragment_track.xml @@ -5,41 +5,35 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".MainActivityMapFragment"> + tools:context=".MainActivity"> - - - - - - - - - - - - + + + + + + - diff --git a/app/src/main/res/layout/fragment_tracklist.xml b/app/src/main/res/layout/fragment_tracklist.xml new file mode 100644 index 0000000..9945328 --- /dev/null +++ b/app/src/main/res/layout/fragment_tracklist.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_tracklist_land.xml b/app/src/main/res/layout/fragment_tracklist_land.xml new file mode 100644 index 0000000..d9b49d2 --- /dev/null +++ b/app/src/main/res/layout/fragment_tracklist_land.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/main_onboarding.xml b/app/src/main/res/layout/main_onboarding.xml index 95247fd..a20a16a 100755 --- a/app/src/main/res/layout/main_onboarding.xml +++ b/app/src/main/res/layout/main_onboarding.xml @@ -19,7 +19,7 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/activity_vertical_margin" android:textAppearance="@style/TextAppearance.AppCompat.Large" - android:textColor="@color/main_onboarding_text" + android:textColor="@color/text_lightweight" android:text="@string/layout_onboarding_h1_welcome" android:id="@+id/h1_welcome" /> @@ -49,7 +49,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.AppCompat.Medium" - android:textColor="@color/main_onboarding_text" + android:textColor="@color/text_lightweight" android:text="@string/layout_onboarding_h2_app_name" android:id="@+id/h2_app_name" /> @@ -57,7 +57,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.AppCompat.Small" - android:textColor="@color/main_onboarding_text" + android:textColor="@color/text_lightweight" android:text="@string/layout_onboarding_p_app_claim" android:id="@+id/p_app_claim" /> @@ -69,7 +69,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.AppCompat.Medium" - android:textColor="@color/main_onboarding_text" + android:textColor="@color/text_lightweight" android:text="@string/layout_onboarding_h2_request_permissions" android:id="@+id/h2_request_permissions" android:layout_marginTop="@dimen/activity_vertical_margin"/> @@ -78,7 +78,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.AppCompat.Small" - android:textColor="@color/main_onboarding_text" + android:textColor="@color/text_lightweight" android:textStyle="bold" android:text="@string/layout_onboarding_h3_permission_location" android:id="@+id/h3_permission_location" @@ -87,7 +87,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.AppCompat.Small" - android:textColor="@color/main_onboarding_text" + android:textColor="@color/text_lightweight" android:text="@string/layout_onboarding_p_permission_location" android:id="@+id/p_permission_location" /> diff --git a/app/src/main/res/layout/track_element.xml b/app/src/main/res/layout/track_element.xml new file mode 100644 index 0000000..e5fac85 --- /dev/null +++ b/app/src/main/res/layout/track_element.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/track_management.xml b/app/src/main/res/layout/track_management.xml deleted file mode 100755 index fbee446..0000000 --- a/app/src/main/res/layout/track_management.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/track_onboarding.xml b/app/src/main/res/layout/track_onboarding.xml index a12abb3..d42787b 100755 --- a/app/src/main/res/layout/track_onboarding.xml +++ b/app/src/main/res/layout/track_onboarding.xml @@ -1,35 +1,48 @@ - + android:id="@+id/track_list_onboarding"> - + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:layout_marginBottom="16dp" + android:text="@string/track_list_onboarding_h1_part_1" + android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6" + android:textColor="@color/text_default" + app:layout_constraintBottom_toTopOf="@+id/trackbook_icon" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.532" + app:layout_constraintStart_toStartOf="parent" /> - + android:layout_marginStart="8dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="8dp" + android:text="@string/track_list_onboarding_h1_part_2" + android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6" + android:textColor="@color/text_default" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/trackbook_icon" /> - + diff --git a/app/src/main/res/layout/track_statistics.xml b/app/src/main/res/layout/track_statistics.xml index 3b7ef90..b4f2e95 100755 --- a/app/src/main/res/layout/track_statistics.xml +++ b/app/src/main/res/layout/track_statistics.xml @@ -1,9 +1,10 @@ @@ -11,270 +12,299 @@ android:id="@+id/elevation_data" android:layout_width="wrap_content" android:layout_height="wrap_content" - app:constraint_referenced_ids="statistics_p_positive_elevation, statistics_data_positive_elevation, statistics_p_negative_elevation, statistics_data_negative_elevation, statistics_p_max_altitude, statistics_data_max_altitude, statistics_p_min_altitude, statistics_data_min_altitude" /> + app:constraint_referenced_ids="statistics_p_positive_elevation,statistics_data_positive_elevation,statistics_p_negative_elevation,statistics_data_negative_elevation,statistics_p_max_altitude,statistics_data_max_altitude,statistics_p_min_altitude,statistics_data_min_altitude" /> + android:visibility="gone" + app:constraint_referenced_ids="delete_button,edit_button" /> - + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/sample_text_track_name" /> - + android:contentDescription="@string/descr_statistics_sheet_delete_button" + app:layout_constraintBottom_toBottomOf="@+id/edit_button" + app:layout_constraintEnd_toStartOf="@+id/share_button" + app:layout_constraintTop_toTopOf="@+id/edit_button" + app:srcCompat="@drawable/ic_delete_24dp" /> - + + + + + android:textAllCaps="false" + android:textAppearance="@style/TextAppearance.MaterialComponents.Body2" + android:textColor="@color/text_lightweight" + app:layout_constraintStart_toStartOf="@+id/statistics_track_name_headline" + app:layout_constraintTop_toBottomOf="@+id/statistics_track_name_headline" /> - + app:layout_constraintTop_toTopOf="@+id/statistics_p_distance" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_steps" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_waypoints" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_duration" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_recording_start" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_recording_stop" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_positive_elevation" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_negative_elevation" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_max_altitude" + tools:text="@string/sample_text_default_data" /> - - + app:layout_constraintTop_toTopOf="@+id/statistics_p_min_altitude" + tools:text="@string/sample_text_default_data" /> + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_bottom_navigation.xml old mode 100755 new mode 100644 similarity index 50% rename from app/src/main/res/menu/menu_main.xml rename to app/src/main/res/menu/menu_bottom_navigation.xml index 701fc84..a1fdb8d --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_bottom_navigation.xml @@ -2,13 +2,18 @@ + android:title="@string/tab_tracks" /> + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index f1b9414048cacc4190a19b7c04907b994baf5768..c4861484d4e1d6b4ca16c4903d1f2109b3388bb5 100644 GIT binary patch delta 1757 zcmV<31|s>>6YLF;BYy_7NklzE*D6|w1kQU|^ zoyP2@$=uvD%chyjbaN)k>UHXzB7!cWz{Keo8o?@aA|m%|D2=ahY?v$pngd z_nn`@h!IK)Wu^QldDD`%q~Fu;91_m?D-iI5AN=44KgRZeK!4zn7al&*p2BJxDVzy* z{J~Ecv0rdWBAizyaoKiJB8)Ecafzaz_gEv%3b|F%6e^EuR`^BJ?Y3s6|GZ%%^}euN z?mm@Z@-9kTz@oKs8WeJ?r;#O8o>v|&pK1|O|M^KM`3y!QaWRZW>J{=)2}7z1@hub> zam-qi*doSmOMktE^Luf$qtHF0)j`HCGU#W@km)a8*uQ3TfE)_FguCn&h+?P<^+Tpy zyiZ{KD{U^&gdDN+L9LwvaZ`m=qL}w1MxsSklh2(b5JUAMSro6D~edyeM$+X)cAOBECs6X zmpV!X(JTGA`?bc$Z3>7LT|y`YAu?3O?uGtX5-N6XhvvigQM_P23d4esFZDp7 z5w91m^gd|!V)R1PauEK|QL4fMp?i5YYL~r(rVVTHOJW@U$j!oUsmGyvDV&34 zc8dy?^x8UG0b-v%n@Z<>J&?O00WX^gtE`iLMYU37Ys8elR z0}Taf+yj|fkH$Tasr85lGBMS7AQMPDkcst(2Qsl9@jxb~8V_Uwi3c*V9`QgX)*~Ls z6cTk34>VXhI#3dAga4CeyBcR?o=~q`fq&+vrvDA-PJ26Y)~`c8x2s`8Kx|(uUFv~& z#SG-9rl4sUNS5NX-@XlfZVr-Xhoe{#z->p@+se)=+e*&%b;q}UUf3NTipZsJBX)xd zDG3S4O;1NzNeQa;dbA8-`k8|0plNjD`gOE5Hlj+aK^A==NxQzp{uRp+JNHGT(SP*1 zMBP(sYkTu-9b#Q{l#rqWeDQgpA0mSMu|^S$h>!@(3&2$BhF!OPzI^&{7C3N#=z^et+JG^_Os+ zwj)r6O*4$$MsTXInOs-wpu`Xc>x`q66NHgQ7#Re?Fj%K(2!lxydbY*5d%83qP_1dj*efp{Q01R4j| zYMhnv*J=!UIn=q~bkKQ`^Im(upzf5^Gr?k@M3-qxjBnha^|l`%);a8w%&HJ6Vno6f ztk!tBe3H2r^*%sxO7omvq45^7E1nJ7Q3(5Ft(`{W^h%S7s;I_iSbwc%f6+in50In6 zadOx(_h+ke#LuCGpfpwvd*jv&n^r#tOL1Bzx}tpS(OImV70KCtnqyy4t5fL@Pi-x7!%`PBb8rF)ytDu00000NkvXXu0mjf(r`@O delta 2506 zcmV;*2{rcY4bu~lBYz2+Nklh8OK+f%=9o*XKD{?J6iAp1<{437&**j zYP7M&Caub`yK<#EYU3C$LK3a9f*^>92rAw*5k;&b5oEa+5CoRPUDyk}QMnB_^Y8P1 zyC5+VIpkQ_cjh-A?0&%e{PuaC_glX2TOQ9a48t%C!!Y!FfPcpulW1@M+DL-M^s3~H z9l6DPn5h{o)H4+qOX&TOA?Tet-;!LL;&K$uR#0166Gd<2? z?USH*(p&$%#G-x@IytY{X{}-{ho0kU@|^kbgmtLC|Oxl%XT@Pv$5J+y~j| z6Da)h3*>o-kh6R-GHom&nKKO%^QkXIPW!$TJ1Z!bzJoGXN8I)N1dqdlQ5zqN%CAnN zbdeo92;>-YK^%MiA51-|ld!#A5ZA=l3b56?xQJ|_!}Wg4_pR-&!B z8SU-u9IfbXE)CgXWA&CM;StFOnQ z&|}(8i`FR94lX?9P1@%avS)a9N_z z;Lg2=sHv?(OKU3$`F@6V6%?lrg5)Js1_edPxRC>KN;|jEQXsgoP#=}QX@U)?Ulk&0>)Ehx;%fuukrQ%vb65_;#fiW2l5uP*{9xK$dwu z732tY{`=5Km!ULQ0Gae4G@61}w|~sjYSw(a9F4g*@ig`-8uRn@O^^U;=?9!3EhMXG zEi{Tt6uh$Ef@;s4!d)9{+_ARes6BaH&w9{)>OrNKp^^DOQ?ROwsO8Yy@`Og6)h|KS zL4loe7~?A=aZLw1I2nS|Nq*U9yQXtC-48l}P(&M#n0~1o>QXBpl!y zBfyb_EfD8LJrl*sP98{d9Dllqa;C>ZJIij=2-M3&F_HSq18U5T zqeoCzQD>5OA^TPey%D7*uD(gDogc~hJ143A$qFr19%!%iLwkdmWPd&GQD%(@K^^S7 zDnfL|A;^_Q+Okv7@l&DbnV_yPS@j`kZ<3M-_P?LDkuRu&kob?0UU*H{#7V%@vn1qW z60@=!S@hhuQIu7WMu#AVAFX(iX#tQ?HgVa}){ORM8D-5060+e1vgrFtA@!^MlvTO9 ztkVAXF?2W!TC+_A?|=XGUXT_?1jnsM{{2`ytW%<;jRj2yf^D@VA%|oHldLu0&!|dg zwAXw=jXfRBP4(EjZ~yQWv`Q$1XQ-ebk{od4hC5E(+<@rf-6(vJfZE26Qfkqb6>Hix zJ4uYwcSJQ;2#}HNMe}eUzJmVYAb^8N0M~yBvF>vrHU|js*?%!-{5!%4|A}(yQTZLY z;nvB~Y!7@b^+ZPbIaJiEsf1d%K2uZu0OhJH$j?}Z*o)2x4Hm%bQvpP-0&Wh&TF~>X zbP~eJO^DU&g!pKy5F5QI%MJ^$^NbU|jC6$eCC6?Bg-YCUQ0#_)Sa$@R--yGf4#3AR z09&?u!DH=utbcM|{ldRFEY50loEN)jX$Cie3pXJLfh$%x3VVcuPz2E$5j-}Duzsfq zTLXpIaZ(8X%O2Qy+#Q?ux?!!S3tZN_z)>XX`R}DUu(`MkT_Lu!9=h{l8D>+FLd06k z#GZHmjMXA{2h^B0cB3b<*fdcD0RuB`ZL=K5z z*&`q7%U>kyA_fwbWUkF7@tpo~lLG52EMxKrDG4an$tf95SD3MklJpb#0)F4=BBjJ| zjiTWqPJcZRGz#w3xH8vpuf`AF4t2FZ8T5_Gl>H<9hPo(=nPGhvbj)~?KsWK$%-JJH z5X&5POQv+b1p-Wr*05MZ`TW;=dsFX^h>6iNC+B9&Hez=?w`L9k?3Ep86uRKtn#caaB;3lf$cAT{x*wo z2W3#7?wz;i@qSF3-x3y^CjHVgaLUB9hm0m(2r-%%8Dcc){6G+4KBJSf&wYLN1<&<# zpEG7NBL8bX?Lzphsb@c%I@y!O#A37N@qW^u4__F==kb0@+usn=h*`vJJ!h6KmS3Cm zctbzqy^7E9AW5%Vj2}OK`WtUdFdjGVSNaT#fyKgN8i>yW88bpK48t%C!_bTIe}sr$ U>^cH6RsaA107*qoM6N<$f{`!H+W-In diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png index aefb6ac17da3280649440a49296750e5f97eda6c..66dca23d21431428b3ea3936df7be7a8cf44584b 100644 GIT binary patch literal 2787 zcmbuBi8tF>8^?7_b-Eahy$mYUT8u$jlTnOxQEMmtQB_nrL}^M>qM|`-Xo%9WR*_g^ z2_cb)U2W}@T1wI=qKl@8ZB!$u2c)E= zWF61J{t)-Y-|7!C;0c^R5Pb4~1(4c(4>QEPeY@@h2EEp}*?nEsn+Qd*qF|51iHjUtG> z@8Qq)1dwabQof!^)5Pu8`FFQpea=(*)nm5x6nCg;Hd%TO1wXNyzqR_gf4{blO_#-OLpCZrVN)uc`YwLO2RE;{FWbBS`Stn4en>HNsSRE>$u+}w>m z(p0qBU`$})wsS0@okpX{ZG2zwvwxS0)#K(fapx5dSxb9Z^WY*6b+$4oEVyL63PM?I ziCj)KHZr<=5e~1fY;WJ`PE1VPE_WlvaA1q`F&o%BfBtpuTt%nF{*FyY3uxWq*K@lIMMGL|hO<;1%a_-u3M*d1y`#6khlRVf#osu3X6&nqOHasqosh396rY}(<9PF;9=6rp;W9p72%n4!t4w0^Dm zqi-q$f52NV(Few-o^toAa64@N9awPvl?-^Ws&k8a6ww%viI;@2gTY#lyuV@w27_f3 z&GY9^!(ct$$*NRCH4tc}0i5V?Mm&H?QWXfr3PcO+kPr(3foME%B05<-weSdEl>Tj? z8=Qz}!{hN|{vZdMFHB6;0ma*#)&Ya3X!rwShN(89?lnQID3CY5Bnjh6P)(6g-C+w0 z->Bw6U`*gQh^i$BTK^4ls0tt1EHRq#k#vcXgxB!-mTe-eCzYxq_HxieoRxsJm1u1e z@FT()Q3C$w^o=12_&9j#rUVQDg)$`=3jUtN$zREQSRz>fH%Mku@c11G1)nr433SF8 zwk%N;gEu8EAe9U7a2(DCjM)ofmc@s9-b-(BhV@!?vnX+!-+^?F7OyGN_NXpQAo*h} z)5s+VXozGR`;0Y|ym=qz(RI9dLiVD%`H$gw;P!PEgOMf{$Z7tz{x>=j_w%rDKh!M& z4qJql^K6-N1#2aFr*POmd}Cq;cwj_CM8v?$T#Jmm(JM_n_#1**{+&pDrq#&k>y3;v z57mUzUiE<}UQ1+X)GbHH)fu42*pq6B3=GU3%C$gJ&>l+y=?>1z)~=>EGDcaN#Q{ne3W@A(obme4FS7 zLn!nsIyg93*c>sRtbF{^qq51&5;QOe9Ch{Co4Bo~gZkrA zZP%A~gPuc8Fy0HSbuQ!jXsW3fy);aGTLCB>9#TzbXJuwj;Td@GDouKOdn1kqRdUb-bE$dRia_qGgZ^EC1$bx*WG30I?S9*=HvCYUeqLPgMMpU{zG zZ&pIGXTZ?~MX;=T@nchA#@5HHHkHp1_28t(j~|~kX}=!-#YVl5rVL-Kf3vo8jG*PU z#r}|!KKenHFre(ch-~m~4PEghv{l(bW*;cX%hPjAeX!`&Y!lK>0GIki$IDB2@8cm$ zW_DH=gt#SR4w#ZOmb2T8WJu>()60w&%@&Ai-6Ng1EkceV6s3@b7;GpmJ|m@Fpji{P}z6-I#Wq-S5uaKA7E`9 zsjnT}BP#{%*?~+SYXcR2>490$ zBP)MT>Z6-+rxf6$*0Vh~5(M>gC6T)B+f4&id!`g=v(M>|W8+=1ugz3+z*ee z%|B5dzAKn}eYNdMf}kY1tdCD2ruVxnyEPjE&*Oz&-oPM~*=#nwEaGw9PL?_h?k2Qn zz}G$9YfIDn)u#+O9bFsLnOLo6jsN|(S2()0@7lq=Px;dSN+6D>U18MUuipO;M}bQq delta 3428 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 delta 1724 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 fP0!MQ1j8(vO3U+jr4Y++4OM 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 KMNUMnLSTaKaFnV5 delta 2994 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 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index c003095ed260fc946214ca20574b24f44de62086..da5f47da5e1f3e086c1307ea7821b323cb922685 100644 GIT binary patch delta 2679 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 delta 3552 zcmV<64IlFV6yY0?BYzF3Nkl%A&HgB!1E18!^1rbva+N|B$ zHZwCXAOpiiSIf0b(^AWHHLpGDT54EnsY$sdiVDa@+Q!JTiObvu)Oh0Z*nn+OJG?w96nBk;+J=hV28F%H7#gDm842NEu3Z z=am}I5l)tP5>iOXZ9ocl=QbfF+qyZ+vd1|&n6(p(m49Mb|DI&J zJVkGQO_HNsZL*_noux$wZgI)ooof|tqiPt0>~SL%AtUXr%+?7;0A%q{5AzZA47F)n=r;n zJk`poyQzn*s{W9{);7rwc30Q|rwp^z@c?e2@gsFKAtPn%@T(hXzHD_D7yvvHZ1;=< z9c(!d;O+pVjU8UGc95+L^UfRq{;FcApOq1Rg%#Gw?=Xg7pUKQ4TVeIL($G{xUl>~C|(!~Is2VK?GAv^^h7c! zffVO75LwHYKy&tN1F0pskRLya-0fSCwRizir+-gIg8OL14j+VAdd=M^dxFbdDaT_B zWUu`d8Y+Sd>sRCI&p)CfGaa?1*Pzqs8tCHIO%P8QjXWcyy9Ln}02s|@#vdE)fK{*m z6Z3ri5pg2C34jJttD#mZk)M}`?35Hlh91EW!Mm|}?RqR*v;^}4<{-dF)I@jNJLRV0kq}UQCD3BU2Sa>(@m9`mX3EtfmkfLiiRs5-#{FG*%P^jIBL@XjMdMwIoQLEI{>|ca6CN1YyeEvTEiK*>VN8L zl$Kq``HPnzS7{J>ECNFR+4cK>;FXEUb{~D`F0eQNCws&bfM^1+)6>2F0A~3GwkZI$ zROL4+t5AIP8Z_q$AywofTO!AqTp6dsVc}fS82A5&S6)IE0gy5PGȾ$#L>6s=-* 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 literal 4977 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 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index ab09819823ff091ae227074b646ec8ec9d307d60..02999b1e843172f7617b145712aa87c5d4d5d852 100644 GIT binary patch literal 4476 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 literal 5645 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 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png index d9971babb7a6ffbc67288eab9f5b636e8d27b3e2..583031c7e934ad61fe1a4aff91219d953ebfd59a 100644 GIT binary patch literal 7763 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 literal 8554 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 XG)LxnWuP(hvyinO=!22>pM?Jp&97qi diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index d763e8230a67ba96300d2ea256510578731d353f..b303b1338cc25d244835a057b24685c1a09f5358 100644 GIT binary patch literal 9412 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 z z$|7WXgyds^<{G^ zy;yz{!0OAaC71=;PzHmzLfQ9%*>n4fKnY;K(~tcPZ}vC4TiBBa8aD?@43z+u7aMRN zHu%0wHCig+`#$VvyhNagVetTSjX-4yS62(r#MRPsEPu~6_xL}{(52Oeq5a7K0000< KMNUMnLSTaBpwl)0 literal 10657 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 Hu0mjf!TUVo diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 524dad21f261ab267b8d74280b909f58c1588a8a..4bdccc52ef72a9fd266082d3990f39517bb2e49a 100644 GIT binary patch literal 6608 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 literal 7734 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 literal 12208 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

FG^FzU+&(Cp`R6HE37mPU|7z zVe;R5DqB_05bMxsTQ&vNC)~4T?Ukxk?NxSbRq|&)rTjTZB7i`e8YOwZyHMxMi1pdT zn-?ygn!4ECt9(LR^_0!`J9JyG;`%a z>@sjZwSKH+q2*A(!u;0bu|q}CY^cfqz(2leIfk6U!#{c9Fg7;xYi4<7Yq(MRns)Fw zVopXS`RO(wQNJk4PbT^-PAl||De1E!;~(l1nKmJscBD=7+@l$@E#`w1wbhkZ?rL8y zrz#eQCC4)Dc~|*zv!7L%G_U3@I_Bm&+!q>{T0uhtuiWvv+vd83ScF=JPKKy_JF32Z zuZ3yAH2oJT+Wpr%4)=vK6v*(f^IC;6uQum)&s%wqe6z1gK6yPqJIQIA`AMSvywQ%I zFT6fIlg#sFY{5n)=?ChX?jdy#m{a~ck^{)RR-V%aYsEe*g z)ECt^9=~u4U3K%$sPf!MK(o*hAtgr&v_|OshQBFY+U$|88$EGw^vSfj+Oqrl_37vs zg_5?GjdP)aRHLmjb9aO{!ckI1L{|1d;O&8B@Rk&ot8JngxxbK^%k)=b25GftE8#7N z6#rjXXAZL#SDO9%wkh$6t995#H?RG}xfR0o=)O-uOT=fTDD5i|ZMpik zSFL)R%W)wMM(?O0`O0Po@56?uU=vhxO}9+n5ffv}b{>}cD4lhU-r^Kc7iGI%Rb}zP zvT>db9hY}*8JOLS&BEkuz4+tw_IIGX+{$IzKi3$$LTI z3k~nbt)%J4;kVX+UFttVNkdGZiS?Y%f>bMh#ztAr-5H2q*_3JB+g2OT@+V21AEdsD z!IWd3?)ac2`gX_Aj07#-3cbG_h%gmnL+kyReA zyBxF^RT24=cm8lbWN25jRia+E-g;;BC)cE7!%KtEw#?qA-JY*l@Zr(m4SFPcBEU)X zr&yo8U2LZFWXV)K{`KY{lSg26ut+-`Pl>Y1aO9ckf3b%~Z0pIN%HMDOGXjF=_YP{8 z6ZQ!rrN334LEZ$Dq=)MVWT&8CK%!^fSN^0V)|RtI!yZScaN6u5D*_!Ox7Zg0I2VcQ za_CxX^VeBjvz#HMw)tg8U%?1iO-}Ku5Z1BoY>Y!ZmnH8|!#XRS`nMf|sSGRIEzcp!$1m@LWZk*kR&FS-o0u^C|oP#Rtv* ePl?WXjdMCS+bQ2F7Hqv%Oi$ZbtMWcL^8Wz2;g~J} diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index 3689a3226b11e9043f5fd10de8d3bbfa7843c2a2..afc28dff985363e673dcc054fdea963c56fbb569 100644 GIT binary patch literal 13894 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%0AR+d z6(}ZaxI+MKr8mrv!8p(f%|T3o;2LJH^Jk0XSkValEn0 z&Bu%cDUtq&@Ml3l4hX0NkPD&C^osbjk2XlUIv=DRUiUj>jLic(7Lw%hh=%Wjj%krX zk0q%2(;!mjfgIh-Qzcm^g(C;O0LM2$qmJ+}ufk#*pDiXN4HjAQkm&F;?(I|M7NCP|BULKypja_|q4UBVhnBAmp>5dp`ms z)eQ(am_z)UQ$mA3G3r_%` z1R=M8E)!t2vs8I=Bl2Dr?_inZaU3^PdDT$Ln(Q!@E%JjjWF7{*2Y{IcVgO$Lm_sy5 zRH5GS8yjVUGrS{D!EHXIo7NmyHc~+{VPAJCvJGyA@;~A>&mref)mD zc$pMLN)zutks+Fps|J9?u69Nj{K9*z>FeDrJLiki8Ogmqzuw zJ$97#n%L*&mm?1Yb0|9)m<_%%{tC+2Pq`xi?_hI^(@>*??74qU@v2%D^jCG75dR$% z?42Nbv933$NOt)5pp+v>GWGb$&;)0ab=+X~4wm%m>`JU9VQlC#vZgXcWip+Wx6K|GLk z2Dqep&}_(+7xHRE=;rnM?Sr(aDl%-pbGU@5Air0!0E0nHtdMl9>J`A{5V2OghBv9i z2rrZBSEtkqR>1%-V;pa2I#Zy-*){Z1Awah(13pMEsyZ|vE)dpSD~-7jR^si|IU_q(tGVf$udPJVg^pns)Wp#i%DI~Sw(93L&j+}M_S z(LkDQgimh0gGK(-RyHc@8_gpcVUBsRmO_ND%%_tQfsj(5dvSuOEdfzTIgAbTxbL!3 z;b_R_Ak!>_`nZ5~t*A&|-XU2y{`Q}wHNll3Y3{XB=ApQzWyTS*QRb)D*H$ap;+rh* z1N@9x{ERbJx~hV__K2A;ZqNS?^*pFEq$)}(4v33oh~Zk&3LJ@?ut2H%C8*Z1;MY0^ z(sbDyayiPTA>O#_@t;(Ao`gL}^f|&GiWlrAu8Q8yVv==!=e`L&%F0@?zL_#F2#{Q= zOvKq68J7!Bl>ZluaeTx^>nG4z$=_@GUhEb^nBFf>l8*gS%Ar+4ss!AVZ$M*|Cr1OW zK+lnRn(=x%)OW(7b)i6 z^LKMgk(N#a4~6M-_RBVJh3={R4&?5EP+7AHvJH zAmHE(bg5i5U+P?MTyQzM!1!xA1z9m{^Eh-wcK(HO3F)VBFbfR`LDT;HLmbkcd+#79 zSw8~zDQyRLa0QS|ayTz-&&D%r!fUc;%yKP*NY>v;?;nW?M>YvQY@z3Bnb z!ADbX=CRo|RrK9(xt&v{e$|pygFy#`{SclC45$p@Jbejt9uWlQ#y8JU3%-IQy>rUp zV#KLB1Tl<`V%rlrrFI2s^Q%B52O(Mv-Cc` zM}rOme+nm2Ti+hczJPZEQf{#9D|j7lL&V2|g&gVtPYOVWcT^A5|L^|J*@Tw%jygf! zsYbD+6tMEZh?s&QjH~LfM0{bEU#>v=#5Bs6;K|N=Fa)`6c2|8e*kk@ zL@ijw0wY3`<0l-4p^|jWTx>?N0V{5hXRP}%5u3gps#~#7$9zVyXJB7qpo0x$lMG=wyKB!1fJ+d8sVav zQ#7{gj&PLT;nc~~;2PO`qN+E(S}VfrF}ja9)&BR!TOk}d!>v?d(* zk=@ZK+h|}#OM$J)PIhUUsLQ=Hq3UrCfNWydn^`AG@i01gDm2#1d+qJhKIU>ArCHL2 zgDa;7HCe;#k3kKZLKYu!%-e9K+WKF{ZvL@Pwl19MXvGz2lvwXI-#l`F?ZK`eF=tl zPTqlJ*~LFA?&&^H=Z0ruc3Z!z=V6mxvbE9Nr=%0msd*hS?m*}dWBnuNLN8jUy7XQV zeoyo9ykgnlX*|B)KQ(N0i2~*XxLhoe_f7iq1=6}LT_T%k!b)5~SUChW%>MuPb zYAMZp0`^{526VxLkV(4S(NvSFU){DW@y~rznf`>1ju2P}MSeQLo$1~={)F`<7w(U_rg$gnc{czVCiimxkMoJRgAm8lvHSSk|CAFjgM?Dgp zWwT_^REZVm{X@j5uzb|9!JylKC9f(2U+QQkq)C$(A(3l-G((@6nfnAbk>aHuELj#l>lV+x*cI-IVak6Hx zh4YG&w5508X>k*fn!`WnE}H`%Z&q8fWA1b12bxAvxB^QDqo2=H3q>Rud5G~_V~Q!T zt21srumFx($dSE2?S0v&F#I=bIr~aU$L+}33;p=aOUyAYK9F>tIh!2!lqj*0RLv_F z5TrrVq(SMAe(r*OO;jnVJ6)K4u%@a_ZkPCr!$JX#{AFa(%{$njgC;kVR&rGdkjYQSWWZ3(YW)wJ);5YS-}VB|M0ne~m;$XK=% zxF9uYSal*6j0bZS{mhW3{Fzu9u+}Crmr<15pD|N9p+Chsit+8JK281g$2t+;cXZ{& ze2?E!H>X{5Xze&EvA1@ah4p*L$~!f)p+>*a#p#b3)~rpsf)8KU7zYXe)(EZlUyX)Y zJNJhRJ&@NoS_YvKFBis_{Cs@oN8heW>O`IRNcNyOB&y-&!POPSG7T(FS{#;k4YrM1 zKHg4Xr^YNl>~0y0Uh0+FCN929(j4d=q(Pf}#8$uzFN8F#BmH>K*vRS~(byjAm}xt1 z-eDeFT}8OCIg`h7FKr{!8I?|P^zu&>74WxCwF-5b7qv=I@^nI%y`AaR{OJaKp|f9I zOXMT$^nfF}cjlW{L2t2(o_y-F9f-(=K>5G$f}T+c?s)#sJZZaG_{3T3m zq(uu&v!#!HJ3b|OUCZrSFD!J8X6`Oia(S8l2Az+^T(cUo@GFWKa@@?k1tE^ST6=Y z?@48zOchBV-gt8Z(m}N@PIt#qZ#&$Xqg;?fu7Uon0FQhh_u$XoddH8mzh1lz5+!-? zveM-Q>wpUWz6ugpb9zg&DQs- za^uIVh5~9%>;TMD1KHdxgeHw_UPTfeMn zm%^P*-fnZgnrbJbZnQ$<>rXSf-uh}E^TZjXO?z_Eq`WkxEZ*ya1aR`ysaq30T72()`w&v-QCOMG8s80opZF=Y1^CIMzW zaIo}*~ua!ANt?phfLGxwhWJ%?yGN8Q>J%-7uJ`~ zR#DR*MeVFzmyOroqd0w_#AI^7H|41p_zfrnBCkD^W{}v7+6#UHdH(hH9i8HLNY_4J zkEqsGQp)VbySu}W%pbD%zQR3vi!Q;Yb2l}5*Z=-?2R}&TB1t_61K0R6>503Hc?~|@~{b*$y5bT~UEg!EI zKjN%m6qA31oCSquo2S#Svoxva?tU9{rw0ViZ$2CgD0pM**m@^EjOCo(qRhR##7TlO z*i`+$+Ka*HiaDbs`Uf3WF>8-rIb4%$jC}2|+}~qs%eXtvPq)K53BY&=6nwkx<=(62 z{5ICDxV1cX<(Kz#tm$f(^r|A8bakZS!26w}9uhY#^h>;#Ev{y(W$TI~q1%@(d?mx8 zSln-pvaQ$s{thyWNZ;P;qWxC5oqe8fL1(*%wY0Q^<02Q~e_`mz$V&M2*<+F7qcvF~ zcQBPIeZ!OD+;gjslIu<|O0%=alKew1>RrQlQZUO~Y8w`BrlBQ%9 zZz~yoEGF3ec&YR--{(@=vAI9fwNNLqTL?T+3=QACXiN?-3^R6zf7~Sgb@Tb4G zUn-#E4_-QqQ&x)M=AASK(1Mp>b{eJ47-F$u> z9j3)YQ*|NX)yq(t`|n!jtKqi;}{t^E2}%~9~mh*im&+Xwykll$z_@|hFXuR z!!8G*egGl?QALeP|3+k!lvd<=se>hCP2+JExMT_3eC-?=yYJ*lRCBs2pW4160;JD+ zv>2CWAy+rw;^b<_O6ai9k0sUPVwb(W$OXp*xq1{^3yM6MKXwd&&iKjd0?A~|g`bl! z$xGMsZ3sP2zreIcSY7+B+?&W9?X%WaGvu&*`A}_9^fGUlp1(WyohiUBpGaCvkZ2m_ zn5b`4$;)W+>5HB~oQ~YsFfLy6Z<#CQ>FvRvb37p(jXhz5Z`)X7I%CU&zi_~;QD}JR z^$PWdf258scG9k;N>(!F=tG;-6QhM17@w!C<{M#!=P3;8WB5Jp-(yca^&=L~Zb~F8 zaOv@T=CGlzmhi!enu0`RcD$EC2j0O+8y$B9$)R=ZKZr71k!F@Z@W9y~JHyn@qgS#` zT$QC11!06XT}nnoLfWnsbZvtHG=U$GJ@REHIyf|>0hagAA3(1$w)RGi-iaR!4=Uru z-0QqrDrG&c4Jm#@K%^Gj5t`wc6IKPGwqxRYV)h-nlGRakupTz(m z0Z5W2{=5zT##!`4QJ&*Q86PHG!_#K#2VcGCDkdVwMP+#F;P$S|CR$T zmQ6ZSsrvW~3uV#Mp%=?hX#p2j9(`d!5h%&HPD*ENi+^fK#WpI2PR{EvkAKzH`f7c6FBE|S>mMbtkI8|YzWX)5~TB4ETSg+QEdFE$ntJFow&oz?VC|z`S%~C$r-kYp&4~fctpv&=DcuP<=Db%u zu(bttF;W;^oNi(1a)9p}w)QsX32(n;`;%Qin+T1SR-U_?W!|ophivwshgQ_h&63^G zX~)Zj@xK_7K#a2hFL%dpb5_sWg^ab`GYozE^Ml&~K9al7B&tRD1rLG`dbM}_`A4lQ z(@~YM_R21}9ES}0`7k@&BwT7FY_5G>0EEr}m!tf%YbPT#;cEc=%b;5=;z@9wGA*nBctee6(vL>O#M*(qF*B$v~P9j$~bv_wg^#2lg- z1}*Ns|9s>_cjgefa|z2`S^RL%?t-cl&%|+2JIdebk5n_SM(?2@4k#fP&xo1)5L`&B zBV7@Gbhtb^=fh2ZnJd}*alR7El(_wBhmVE58@}rL8sTPRUPHRSU2V2lPVz{hlhNW~ s$~m~6IYLMs@(&gwhUB{!kB%Qm`UiJfQ1PUX|Azs}^6GM5Wz0kV5C0$}jsO4v literal 15172 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 diff --git a/app/src/main/res/navigation/nav_graph_main.xml b/app/src/main/res/navigation/nav_graph_main.xml new file mode 100644 index 0000000..7d020f5 --- /dev/null +++ b/app/src/main/res/navigation/nav_graph_main.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/navigation/nav_graph_tracklist.xml b/app/src/main/res/navigation/nav_graph_tracklist.xml new file mode 100644 index 0000000..b8e3edf --- /dev/null +++ b/app/src/main/res/navigation/nav_graph_tracklist.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 89886d7..5abcb18 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -20,7 +20,7 @@ Sporing startet Sporing genoptaget Lokalisering er slået fra. Trackbook kan ikke virke. - + Ryd Gem Fortsæt @@ -39,7 +39,7 @@ Eksporter og overskriv Del GPX fil med Kan ikke gemme - Trackbook har ingen rutepunkter endnu. + Trackbook har ingen rutepunkter endnu. Fortsæt optagelse Tilladelse givet. @@ -89,8 +89,8 @@ "Trackbook bruger osmdroid, til at lagre kortfliser på Androids eksterne hukommelse. Du kan finde dem i osmdroid mappen i roden af filsystemet." Forstået! - Dine optagede ture - ... vil dukke op her. + Dine optagede ture + ... vil dukke op her. Kortlægning af nuværende tur Kortlægning af sidste tur @@ -102,9 +102,8 @@ Ryd knap Fortsæt knap Overskrift for statistikker - Skift mellem optagelse af data visning Liste med yderligere ture - Eksporter tur knap - Slet tur knap - Del eksport som GPX knap + Eksporter tur knap + Slet tur knap + Del eksport som GPX knap diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d757c67..a097c96 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -21,7 +21,7 @@ Aufzeichnung gestartet Aufzeichnung fortgesetzt Standortdienste sind deaktiviert. Trackbook wird nicht funktionieren. - + Zurücksetzen Speichern Fortsetzen @@ -40,7 +40,7 @@ Exportieren und überschreiben GPX-Datei öffnen mit Speichern nicht möglich - Trackbook hat noch keine Wegpunkte aufgezeichnet. + Trackbook hat noch keine Wegpunkte aufgezeichnet. Aufzeichnung fortsetzen Berechtigungen erteilt. @@ -90,8 +90,8 @@ Trackbook nutzt osmdroid; osmdroid speichert Kartendaten im Externen Speicher von Android. Der Karten-Cache befindet sich im Ordner osmdroid auf der obersten Ebene des für Nutzer sichtbaren Dateisystems. Alles klar! - Bewegungsaufzeichnungen - … werden hier erscheinen. + Bewegungsaufzeichnungen + … werden hier erscheinen. Kartierung der aktuellen Aufzeichnung Kartierung der letzten Aufzeichnung @@ -103,9 +103,8 @@ Zurücksetzen-Knopf Fortsetzen-Knopf Überschrift der Statistik-Einblendung - Umschalten der Anzeige der Aufzeichnungsdaten Auswahl-Menü für weitere Aufzeichnungen - Schaltfläche „Aufzeichnung exportieren“ - Schaltfläche „Aufzeichnung löschen” - Share-Taste, die den Export als GPX anbietet + Schaltfläche „Aufzeichnung exportieren“ + Schaltfläche „Aufzeichnung löschen” + Share-Taste, die den Export als GPX anbietet \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c40a7a3..406ac89 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -21,7 +21,7 @@ Pistage repris Pistage démarré Pistage arrêté - + Effacer Poursuivre Sauvegarder @@ -33,7 +33,7 @@ Supprimer cet enregistrement : Supprimer l\'enregistrement \? Poursuivre l\'enregistrement - Trackbook n\'a enregistré aucun point de parcours jusqu\'à présent. + Trackbook n\'a enregistré aucun point de parcours jusqu\'à présent. Sauvegarde impossible Exporter Exporter et écraser @@ -90,11 +90,11 @@ Trackbook a besoin des données GPS précises pour enregistrer vos déplacements. Si les données GPS ne sont pas disponibles ou insuffisament précises, Trackbook utilisera la localisation à partir du réseau cellulaire et la triangulation Wi-Fi. Trackbook utilise osmdroid, qui nécessite la conservation de tuiles cartographiques dans l\'espace de stockage Android. Vous pouvez retrouver ces tuiles cartographiques dans le dossier osmdroid au plus haut niveau du système de fichiers de l\'utilisateur. - Vos parcours enregistrés - … seront affichés ici. + Vos parcours enregistrés + … seront affichés ici. - Bouton « Supprimer le parcours » - Bouton « Exporter le parcours » + Bouton « Supprimer le parcours » + Bouton « Exporter le parcours » Bouton « Enregistrement des paramètres » Bouton « Démarrer l\'enregistrement » Bouton « Arrêter l\'enregistrement » @@ -104,8 +104,7 @@ Bouton « Sauvegarder » Affichage du parcours actuel Affichage du dernier parcours - Bouton « Partager l\'export GPX » + Bouton « Partager l\'export GPX » Entête de la page des statistiques - Baculer l\'affichage de l\'enregistrement des déplacements Liste déroulante pour la sélection d\'autres parcours \ No newline at end of file diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index a7fd780..30b026c 100755 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -24,7 +24,7 @@ Tracking resumed - + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index fd778d7..dd6e399 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -21,7 +21,7 @@ Tracciamento avviato Tracciamento ripreso La posizione è disattivata. Trackbook non funzionerà. - + Cancella Salva Riprendi @@ -40,7 +40,7 @@ Esporta e sovrascrivi Condividi il file GPX con Impossibile salvare - Trackbook non ha registrato nessun waypoint finora. + Trackbook non ha registrato nessun waypoint finora. Riprendi la registrazione Autorizzazioni concesse. @@ -90,8 +90,8 @@ Trackbook usa osmdroid, che memorizza porzioni di mappa sull\'SD esterna di Android. Puoi trovare la cache della mappa nella cartella osmdroid al livello più alto del file system accessibile all\'utente. Capito! - Le tue tracce registrate - … verranno mostrate qui. + Le tue tracce registrate + … verranno mostrate qui. Mappatura della traccia corrente Mappatura dell\'ultima traccia @@ -103,9 +103,8 @@ Pulsante Cancella Pulsante Riprendi Titolo della scheda delle statistiche - Attiva/disattiva la visualizzazione dei dati di registrazione Menu a tendina per altre tracce - Pulsante Esporta traccia - Pulsante Elimina traccia - Pulsante Condividi che permette di esportare come GPX + Pulsante Esporta traccia + Pulsante Elimina traccia + Pulsante Condividi che permette di esportare come GPX \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 6dfeccf..a475f88 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -21,7 +21,7 @@ トレースを開始しました トレースを再開しました 位置情報がオフです。Trackbook は動作しません。 - + クリア 保存してクリア 再開 @@ -40,7 +40,7 @@ エクスポートして上書き GPX ファイルを共有... 保存できません - トラックブックはこれまでウェイポイントを記録していません。 + トラックブックはこれまでウェイポイントを記録していません。 記録を再開 アクセス許可を付与しました。 @@ -87,8 +87,8 @@ Trackbook は、Android の外部ストレージに地図タイルをキャッシュする osmdroid を使用します。地図キャッシュは、ユーザー向けファイル・システムの最上位レベルの osmdroid フォルダーで見つけることができます。 完了! - 記録したトレース - … ここに表示されます。 + 記録したトレース + … ここに表示されます。 現在のトレースのマッピング 最後のトレースのマッピング @@ -100,11 +100,10 @@ クリアボタン 再開ボタン 統計情報シートの見出し - 記録データの表示切り替え ドロップダウン メニューでさらにトレース - トレースのエクスポート ボタン - トレース削除ボタン - GPX としてエクスポートする共有ボタン + トレースのエクスポート ボタン + トレース削除ボタン + GPX としてエクスポートする共有ボタン 夜間モードに切り替えています (長押しを検出しました) 日中モードに切り替えています (長押しを検出しました) システム設定モードにしたがって切り替えています (長押しを検出しました) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 98acbec..9a83ade 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -21,7 +21,7 @@ Sporing startet Sporing gjenopptatt Plassering avskrudd. Trackbook bil ikke fungere. - + Tøm Lagre og tøm Fortsett @@ -40,7 +40,7 @@ Eksporter og overskriv Del GPX-fil med Kunne ikke lagre - Trackbook har ikke registrert noen veipunkter så langt. + Trackbook har ikke registrert noen veipunkter så langt. Gjenoppta opptak Tilgang gitt. @@ -90,8 +90,8 @@ Trackbook brukerosmdroid, som hurtiglagrer kartfliser på Androids eksterne lagringsmedie. Du kan finne dette karthurtiglageret i osmdroid-mappa på topnivået av brukergrensesnittet. Skjønner! - Dine registrerte spor - … vil vises her. + Dine registrerte spor + … vil vises her. Kartlegging av nåværende spor Kartlegging av forrige spor @@ -103,9 +103,8 @@ Tøm-knapp Fortsett-knapp Overskrift for statistikkarket - Veksle opptaksdatavisning Nedtrekksmeny for ytterligere spor - Sporeksport-knapp - Sporslettingsknapp - Delingsknapp som muliggjør eksport som GPX + Sporeksport-knapp + Sporslettingsknapp + Delingsknapp som muliggjør eksport som GPX \ No newline at end of file diff --git a/app/src/main/res/values-night-v23/styles.xml b/app/src/main/res/values-night-v23/styles.xml deleted file mode 100755 index 5af2c59..0000000 --- a/app/src/main/res/values-night-v23/styles.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index d16b819..f1023bd 100755 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -1,26 +1,41 @@ - + + + @color/trackbook_grey_darker @color/trackbook_black + @color/trackbook_black + @color/trackbook_black - @color/trackbook_white - @color/trackbook_white + @color/trackbook_grey_light + @color/trackbook_blue @color/trackbook_white @color/trackbook_grey_dark @color/trackbook_white - @color/trackbook_grey_dark + @color/trackbook_black + + @color/trackbook_white + @color/trackbook_grey_very_light + + @color/trackbook_white + @color/trackbook_grey_very_light + @color/trackbook_grey_lighter + @color/trackbook_blue + + @color/trackbook_grey_dark + @color/trackbook_red @color/trackbook_white @color/trackbook_white @color/trackbook_black - @color/trackbook_white @color/trackbook_white @color/trackbook_white @color/trackbook_black_85percent - @color/trackbook_black_95percent + @color/trackbook_black + @color/trackbook_grey_dark diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index a5c339b..7b3da0b 100755 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -2,18 +2,17 @@ - - - diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 070e07d..4a37252 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -21,7 +21,7 @@ GPX-opname gestart GPX-opname hervat Locatie is uitgeschakeld; Trackbook zal niet werken. - + Wissen Opslaan en wissen Hervatten @@ -40,7 +40,7 @@ Exporteren en overschijven Deel GPX bestand met Opslaan niet mogelijk - Trackbook heeft nog geen locaties opgenomen. + Trackbook heeft nog geen locaties opgenomen. Opname voortzetten Rechten verleend. @@ -90,8 +90,8 @@ Trackbook gebruikt osmdroid. osmdroid slaat kaarttegels op in een cache op Android\'s externe opslag. U kunt de kaartcache vinden in de osmdroid-map op het hoogste niveau van het gebruikersbestandssysteem. Ik snap het! - Opgenomen tracks - ... zal hier verschijnen. + Opgenomen tracks + ... zal hier verschijnen. Kaart van de huidige track Kaart van de laatste track @@ -103,9 +103,8 @@ Wissen-knop Voortzetten-knop Kop van het statistiekenblad - Opnamegegevensweergave tonen/verbergen Uitrolmenu voor verdere tracks - Track exporteerknop - Track verwijderknop - Deelknop met ondersteuning voor het exporteren naar GPX + Track exporteerknop + Track verwijderknop + Deelknop met ondersteuning voor het exporteren naar GPX \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 35dc049..5d5e79f 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -24,7 +24,7 @@ Spårningen återupptogs Plats är avstängt. Trackbook kommer inte fungera. - + Rensa Spara Återuppta @@ -44,7 +44,7 @@ Exportera och skriv över Dela GPX-fil med Kunde inte spara - Trackbook spelade inte in några vägpunkter så här långt. + Trackbook spelade inte in några vägpunkter så här långt. Återuppta inspelning diff --git a/app/src/main/res/values-sw600dp/bools.xml b/app/src/main/res/values-sw600dp/bools.xml new file mode 100644 index 0000000..8e66f10 --- /dev/null +++ b/app/src/main/res/values-sw600dp/bools.xml @@ -0,0 +1,4 @@ + + + true + diff --git a/app/src/main/res/values-v23/styles.xml b/app/src/main/res/values-v23/styles.xml deleted file mode 100755 index fad0a76..0000000 --- a/app/src/main/res/values-v23/styles.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml deleted file mode 100755 index 544ea84..0000000 --- a/app/src/main/res/values-w820dp/dimens.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - 24dp - 600dp - diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9beeb70..eeb26b2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -32,7 +32,7 @@ 导出并覆盖 分享GPX文件到 无法保存 - Trackbook目前没有记录到任何航点。 + Trackbook目前没有记录到任何航点。 恢复记录 授予权限。 无法启动Ttrackbook。 @@ -78,8 +78,8 @@ 存储 Trackbook使用osmdroid,它在Android的外部存储上缓存地图图块。你可以在 /sdcard/Android/data/org.y20k.trackbook/files/tiles 中找到地图缓存。 授权! - 你的运动记录 - ... 会显示在这里。 + 你的运动记录 + ... 会显示在这里。 当前轨道的映射 最后一个轨道的映射 我的位置按钮 @@ -90,9 +90,8 @@ 删除按钮 恢复按钮 统计表的标题 - 切换录制数据显示 记录信息的下拉菜单 - 记录导出按钮 - 删除记录按钮 - 分享按钮,提供导出GPX文件 + 记录导出按钮 + 删除记录按钮 + 分享按钮,提供导出GPX文件 diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml new file mode 100644 index 0000000..c2dcd8b --- /dev/null +++ b/app/src/main/res/values/bools.xml @@ -0,0 +1,4 @@ + + + false + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml old mode 100755 new mode 100644 index 73443ad..3d65f69 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -2,10 +2,14 @@ + + @color/trackbook_grey_very_light @color/trackbook_white + @color/trackbook_white @color/trackbook_white - @color/trackbook_grey - @color/trackbook_grey + + @color/trackbook_grey_light + @color/trackbook_blue @color/trackbook_grey_dark @color/trackbook_white @@ -13,23 +17,47 @@ @color/trackbook_grey @color/trackbook_white + @color/trackbook_grey_dark + @color/trackbook_grey + + @color/trackbook_grey + @color/trackbook_grey_light + @color/trackbook_grey_lighter + @color/trackbook_blue + + @color/trackbook_white + @color/trackbook_red + @color/trackbook_grey_dark @color/trackbook_grey_dark @color/trackbook_white - @color/trackbook_grey @color/trackbook_grey_dark @color/trackbook_grey_dark @color/trackbook_white_85percent - @color/trackbook_white_95percent + @color/trackbook_white + @color/trackbook_grey_lighter - #FFDC3D33 + #FF595959 + #FF7D7D7D + #FFDADADA + #FFF2F2F2 + #FF414141 + #FF2D2D2D + + #DC3D33 #FFE15950 #FFCA2D23 #FFAD261E + #FF121212 + #FFFFFFFF + #00ffffff + + + #FFE6BA64 #FF3C98DB @@ -38,20 +66,10 @@ #FF4CAF50 - #FFFFFFFF #D9FFFFFF #F2FFFFFF - #FF000000 - #D9000000 - #F2000000 - - #FFD2D6DA - #FFBDC1C6 - #FF5F6368 - #FF3C4043 - #FF17181A - - #00000000 + #D9121212 + #F2121212 #FF3C98DB diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml old mode 100755 new mode 100644 index 60eedb6..5edf8bf --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,11 +1,20 @@ - + + Map + Tracks + Track + Settings + + Trackbook - + + Map - Last Tracks - + Tracks + Settings + + Trackbook running Trackbook not running Stop @@ -15,33 +24,53 @@ Distance Movement Recording State Display duration and distance. Option to stop movement recording. - + + Tracking stopped Tracking started Tracking resumed Location is turned off. Trackbook will not work. - + Location permission not granted. Trackbook will not work. + + Clear Save Resume - + + + Cancel + OK + Show details + No details available Cancel Clear Recording? Clear Delete Recording? Delete this recording: Delete + Unable to save + Trackbook did not record any waypoints so far. + Resume Recording Export Recording as GPX? Export this recording as GPX track. Export Export and Overwrite? File already exists. Export and overwrite this recording as GPX track. Export and Overwrite + Rename + Enter a new name + Rename Recording Share GPX file with - Unable to save - Trackbook did not record any waypoints so far. - Resume Recording - + Do you really want to do this? + Cancel + No + Yes + Remove + Remove this recording? + Decide + + + Permissions granted. Unable to start Trackbook. Unable to access external storage. @@ -59,11 +88,13 @@ Switching to Night mode (long press detected) Switching to Day mode (long press detected) Switching to Follow System Setting mode (long press detected) - + + Source Time Accuracy - + + Statistics track data missing Total distance: @@ -77,7 +108,13 @@ Lowest waypoint: Elevation (uphill): Elevation (downhill): - + + + Delete + Rename + Share + + Hello Trackbook App Icon Trackbook @@ -88,10 +125,31 @@ STORAGE Trackbook uses osmdroid, which caches map tiles on Android\'s external storage. You can find the map cache in the osmdroid folder on the top level of the user-facing file system. Got it! - - Your recorded tracks - … will show up here. - + + + Your recorded tracks + … will show up here. + + + Discard location fixes with an accuracy larger than + Accuracy Threshold + Advanced + General + Restrict to GPS + Currently using GPS and Network for localization. + Currently using only GPS for localization. + Currently using metric units (Kilometer, Meter). + Currently using imperial units (Miles, Feet). + Enable Imperial Measurements + Set Advanced settings to their default values. + Reset + + + hrs + min + sec + + Mapping of current track Mapping of last track My Location button @@ -101,10 +159,17 @@ Save button Clear button Resume button + Mark as starred button + Track export button Headline of the statistics sheet - Toggle recording data display + Track delete button + Track edit button + Share as GPX button Dropdown menu for further tracks - Track export button - Track delete button - Share button that offers to export as GPX + + + 23.0 km • 5 hrs 23 min 42 sec + July 20, 1969 + track data missing + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml old mode 100755 new mode 100644 index 632bb10..3967dce --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -2,18 +2,30 @@ - - - + + + diff --git a/app/src/main/res/xml/backupscheme.xml b/app/src/main/res/xml/backupscheme.xml deleted file mode 100755 index de7b18d..0000000 --- a/app/src/main/res/xml/backupscheme.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml old mode 100755 new mode 100644 index 8dcfc50..4495c28 --- a/app/src/main/res/xml/provider_paths.xml +++ b/app/src/main/res/xml/provider_paths.xml @@ -1,10 +1,4 @@ - - - - - - - + \ No newline at end of file diff --git a/assets/trackbook-app-icon-current.svg b/assets/trackbook-app-icon-current.svg index fb338b4..e61f0fe 100644 --- a/assets/trackbook-app-icon-current.svg +++ b/assets/trackbook-app-icon-current.svg @@ -5,7 +5,7 @@ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" version="1.1" id="svg2" xml:space="preserve" width="108pt" height="108pt" viewBox="0 0 144.00001 144.00001" sodipodi:docname="trackbook-app-icon-current.svg" - inkscape:version="0.92.2 5c3e80d, 2017-08-06" inkscape:export-filename="/Users/solaris/Desktop/trackbook/assets/trackbook-app-icon-current-background.png" + inkscape:version="0.92.4 5da689c313, 2019-01-14" inkscape:export-filename="/Users/solaris/Desktop/trackbook/assets/trackbook-app-icon-current-background.png" inkscape:export-xdpi="600" inkscape:export-ydpi="600">image/svg+xml