├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── deploymentTargetDropDown.xml ├── deploymentTargetSelector.xml ├── gradle.xml ├── jarRepositories.xml ├── migrations.xml └── runConfigurations.xml ├── LICENSES ├── LICENSE_gpl-3.0.txt ├── README.md ├── app ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── icons_readme │ │ └── ic_tower1.svg │ └── webMaps │ │ ├── leafletjs.html │ │ ├── leafletjs_1_9 │ │ ├── images │ │ │ ├── layers-2x.png │ │ │ ├── layers.png │ │ │ ├── marker-icon-2x.png │ │ │ ├── marker-icon.png │ │ │ └── marker-shadow.png │ │ ├── leaflet.css │ │ └── leaflet.js │ │ ├── utils.js │ │ ├── wallpaper.css │ │ ├── wallpaper.html │ │ └── ya.html │ ├── ic_launcher3-web.png │ ├── java │ └── truewatcher │ │ └── tower │ │ ├── AddPointActivity.java │ │ ├── CellInformer.java │ │ ├── CellPointFetcher.java │ │ ├── CellResolverFactory.java │ │ ├── ConfirmationDialogFragment.java │ │ ├── DeeperRadioGroup.java │ │ ├── EditPointActivity.java │ │ ├── EditTextDialogFragment.java │ │ ├── FileActivity.java │ │ ├── ForegroundService.java │ │ ├── GpsPointFetcher.java │ │ ├── GpxHelper.java │ │ ├── HttpGetRequest.java │ │ ├── HttpPostRequest.java │ │ ├── JSbridge.java │ │ ├── JsonHelper.java │ │ ├── LatLon.java │ │ ├── ListActivity.java │ │ ├── ListHelper.java │ │ ├── MainActivity.java │ │ ├── MapViewer.java │ │ ├── Model.java │ │ ├── MyRegistry.java │ │ ├── PermissionAwareFragment.java │ │ ├── Point.java │ │ ├── PointFetcher.java │ │ ├── PointIndicator.java │ │ ├── PointList.java │ │ ├── PreferencesActivity.java │ │ ├── SingleFragmentActivity.java │ │ ├── StorageHelper.java │ │ ├── TestHelper.java │ │ ├── Tests1.java │ │ ├── TrackActivity.java │ │ ├── TrackListener.java │ │ ├── TrackStorage.java │ │ ├── Trackpoint.java │ │ └── U.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── drawable │ ├── ic_add_white_24dp.xml │ ├── ic_build_white_24dp.xml │ ├── ic_check_white_24dp.xml │ ├── ic_chevron_left_white_24dp.xml │ ├── ic_done_black_white_24dp.xml │ ├── ic_folder_open_white_24dp.xml │ ├── ic_format_list_bulleted_white_24dp.xml │ ├── ic_launcher3_background.xml │ ├── ic_launcher3_foreground.xml │ ├── ic_map_white_24.xml │ ├── ic_refresh_white_24dp.xml │ └── ic_target_variant_white.xml │ ├── layout │ ├── activity_fragment.xml │ ├── activity_main.xml │ ├── fragment_add_point.xml │ ├── fragment_edit_point.xml │ ├── fragment_file.xml │ ├── fragment_list.xml │ ├── fragment_main.xml │ ├── fragment_tests.xml │ ├── fragment_track.xml │ └── list_item_simple.xml │ ├── menu │ ├── add_point_fragment.xml │ ├── edit_point_fragment.xml │ ├── file_fragment.xml │ ├── list_fragment.xml │ ├── main.xml │ ├── main_fragment.xml │ ├── prefs_activity.xml │ └── track_fragment.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher3.xml │ └── ic_launcher3_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher3.png │ └── ic_launcher3_round.png │ ├── mipmap-mdpi │ ├── ic_launcher3.png │ └── ic_launcher3_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher3.png │ └── ic_launcher3_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher3.png │ └── ic_launcher3_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher3.png │ └── ic_launcher3_round.png │ ├── values-night │ └── styles.xml │ ├── values-v11 │ └── styles.xml │ ├── values-v14 │ └── styles.xml │ ├── values-w820dp │ └── dimens.xml │ ├── values │ ├── array.xml │ ├── dimens.xml │ ├── ic_launcher3_background.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── prefs.xml ├── build.gradle ├── fastlane └── metadata │ └── android │ └── en-US │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 01_waypoints_and_track_from_garmin.png │ │ ├── 02_map_with_menu.png │ │ ├── 03_add_new_waypoint.png │ │ ├── 04_waypoint_list_with_distances.png │ │ ├── 05_edit_waypoint.png │ │ ├── 06_writing_track.png │ │ ├── 07_settings.png │ │ ├── 08_importing_track.png │ │ ├── 09_export_part_of_waypoint_list.png │ │ └── 10_settings_missing_API_key.png │ └── short_description.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── import-summary.txt ├── settings.gradle └── tower_stateVars.rtf /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/androidstudio 3 | # Edit at https://www.gitignore.io/?templates=androidstudio 4 | 5 | ### AndroidStudio ### 6 | # Covers files to be ignored for android development using Android Studio. 7 | 8 | # Built application files 9 | *.apk 10 | *.ap_ 11 | 12 | # Files for the ART/Dalvik VM 13 | *.dex 14 | 15 | # Java class files 16 | *.class 17 | 18 | # Generated files 19 | bin/ 20 | gen/ 21 | out/ 22 | 23 | # Gradle files 24 | .gradle 25 | .gradle/ 26 | build/ 27 | 28 | # Signing files 29 | .signing/ 30 | 31 | # Local configuration file (sdk path, etc) 32 | local.properties 33 | 34 | # Proguard folder generated by Eclipse 35 | proguard/ 36 | 37 | # Log Files 38 | *.log 39 | 40 | # Android Studio 41 | /*/build/ 42 | /*/local.properties 43 | /*/out 44 | /*/*/build 45 | /*/*/production 46 | app/release 47 | captures/ 48 | .navigation/ 49 | *.ipr 50 | *~ 51 | *.swp 52 | 53 | # Android Patch 54 | gen-external-apklibs 55 | 56 | # External native build folder generated in Android Studio 2.2 and later 57 | .externalNativeBuild 58 | 59 | # NDK 60 | obj/ 61 | 62 | # IntelliJ IDEA 63 | *.iml 64 | *.iws 65 | /out/ 66 | 67 | # User-specific configurations 68 | .idea/caches/ 69 | .idea/libraries/ 70 | .idea/shelf/ 71 | .idea/workspace.xml 72 | .idea/tasks.xml 73 | .idea/.name 74 | .idea/compiler.xml 75 | .idea/copyright/profiles_settings.xml 76 | .idea/encodings.xml 77 | .idea/misc.xml 78 | .idea/modules.xml 79 | .idea/scopes/scope_settings.xml 80 | .idea/dictionaries 81 | .idea/vcs.xml 82 | .idea/jsLibraryMappings.xml 83 | .idea/datasources.xml 84 | .idea/dataSources.ids 85 | .idea/sqlDataSources.xml 86 | .idea/dynamic.xml 87 | .idea/uiDesigner.xml 88 | .idea/assetWizardSettings.xml 89 | 90 | # OS-specific files 91 | .DS_Store 92 | .DS_Store? 93 | ._* 94 | .Spotlight-V100 95 | .Trashes 96 | ehthumbs.db 97 | Thumbs.db 98 | 99 | # Legacy Eclipse project files 100 | .classpath 101 | .project 102 | .cproject 103 | .settings/ 104 | 105 | # Mobile Tools for Java (J2ME) 106 | .mtj.tmp/ 107 | 108 | # Package Files # 109 | *.war 110 | *.ear 111 | 112 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 113 | hs_err_pid* 114 | 115 | ## Plugin-specific files: 116 | 117 | # mpeltonen/sbt-idea plugin 118 | .idea_modules/ 119 | 120 | # JIRA plugin 121 | atlassian-ide-plugin.xml 122 | 123 | # Mongo Explorer plugin 124 | .idea/mongoSettings.xml 125 | 126 | # Crashlytics plugin (for Android Studio and IntelliJ) 127 | com_crashlytics_export_strings.xml 128 | crashlytics.properties 129 | crashlytics-build.properties 130 | fabric.properties 131 | 132 | ### AndroidStudio Patch ### 133 | 134 | !/gradle/wrapper/gradle-wrapper.jar 135 | 136 | # End of https://www.gitignore.io/api/androidstudio 137 | 138 | # there may be some private files in assets 139 | /app/src/main/assets/_* 140 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | xmlns:android 11 | 12 | ^$ 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | xmlns:.* 22 | 23 | ^$ 24 | 25 | 26 | BY_NAME 27 | 28 |
29 |
30 | 31 | 32 | 33 | .*:id 34 | 35 | http://schemas.android.com/apk/res/android 36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | .*:name 45 | 46 | http://schemas.android.com/apk/res/android 47 | 48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 | name 56 | 57 | ^$ 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | style 67 | 68 | ^$ 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | .* 78 | 79 | ^$ 80 | 81 | 82 | BY_NAME 83 | 84 |
85 |
86 | 87 | 88 | 89 | .* 90 | 91 | http://schemas.android.com/apk/res/android 92 | 93 | 94 | ANDROID_ATTRIBUTE_ORDER 95 | 96 |
97 |
98 | 99 | 100 | 101 | .* 102 | 103 | .* 104 | 105 | 106 | BY_NAME 107 | 108 |
109 |
110 |
111 |
112 |
113 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /LICENSES: -------------------------------------------------------------------------------- 1 | License 2 | 3 | Copyright (C) 2019 Alexander Roshchin 4 | 5 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. 6 | 7 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 8 | 9 | You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. 10 | ----------------------------------- 11 | 12 | Third-party materials 13 | 14 | ===== LeafletJS javacsript library code ===== 15 | https://leafletjs.com 16 | https://github.com/Leaflet/Leaflet 17 | Copyright (c) 2010-2019, Vladimir Agafonkin 18 | Copyright (c) 2010-2011, CloudMade 19 | License: BSD 2-Clause "Simplified" License 20 | 21 | ===== OpenStreetMap map tiles ===== 22 | https://www.openstreetmap.org 23 | © OpenStreetMap contributors 24 | License: Creative Commons Attribution-ShareAlike 2.0 25 | 26 | ===== OpenTopoMap map tiles ===== 27 | https://www.opentopomap.org 28 | © OpenTopoMap, OpenStreetMap contributors 29 | License: Creative Commons Attribution-ShareAlike 2.0 30 | 31 | ===== Google Maps map data ===== 32 | © 2019 Google and its data providers 33 | Terms of use: https://www.google.com/permissions/geoguidelines/ 34 | 35 | ===== Yandex Maps API, javascript code and map data ===== 36 | https://tech.yandex.com/maps/commercial/ 37 | https://tech.yandex.com/maps/jsapi/doc/2.1/terms/index-docpage/ 38 | © 2015–2019 YANDEX LLC 39 | Terms of use: https://yandex.ru/legal/maps_termsofuse/ 40 | 41 | ===== Yandex Locator API and cell location data ===== 42 | https://yandex.ru/dev/locator/ 43 | © 2015–2019 YANDEX LLC 44 | Terms of use: https://yandex.ru/legal/locator_api/?lang=en 45 | 46 | ===== mylnikov.org API and cell location data ===== 47 | https://www.mylnikov.org/ 48 | © Alexander Mylnikov 49 | License: MIT 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Tower: a navigation tool 3 | 4 | _Tower_ is a navigation program for OS Android 5 | for finding user location (by phone cell or GPS), viewing online maps, creating and storing waypoints and tracks, with minimal permissions and no background activity 6 | 7 | ## Features: 8 | 9 | * no Google Services dependencies, only necessary permissions (fine and coarse location, internet) 10 | * connects to several online map providers 11 | * acceptable performance on slow GPRS-EDGE networks; may find coarse location by phone cell without GPS; may create waypoints with GPS without phone and internet connections 12 | * displays and saves cell info ( MCC, MNC, LAC, CID ) and the signal strength ( RSRP/dBm ) 13 | * all waypoints are stored on a memory card and may be organized in any number of files (by regions etc.) 14 | * waypoint lists may be exported or imported in the GPX format, compatible with many navigators and software 15 | * finds location only by explicit user's command, so is very mild on the battery 16 | * capable of writing tracks (by means of a foreground service) and exporting them in the GPX format 17 | * tracks from other devices (GPX files) may be viewed along with your data 18 | * NOT implemented: map offline caching, routing, editing tracks, photo and video attachments, serving cold beer :) 19 | * for pure open distributions, that lack API keys and access to some services, there are options to enter user's own keys 20 | 21 | ## How to use it and where to get it 22 | 23 | See [the project web page](http://tower.posmotrel.net) and 24 | 25 | [Get it on F-Droid](https://f-droid.org/packages/truewatcher.tower/) 27 | 28 | ## External materials (see LICENSES): 29 | 30 | This distribution includes the _LeafletJS_ javacsript library code ver. 1.9 (https://leafletjs.com, https://github.com/Leaflet/Leaflet), 31 | which has BSD 2-Clause "Simplified" License 32 | 33 | This program uses several web APIs and loads data and javascript code, details and references are included in the LICENSES file. 34 | 35 | ## API keys 36 | 37 | Some of web services, despite being free of charge, require access keys. As these keys are not to be committed to public repositories, this app may appear in two kinds of distributions: full (with keys included in the binary) and pure open (without keys). The keyless distribution is fully operational, except for concerned services, and always has slots to enter user's own keys. See [the manual](http://tower.posmotrel.net/#external-materials-and-api-keys) for details. 38 | 39 | Builders are free to provide their own keys with their distribution; the easiest way is via Gradle files as BuildConfig.yandexLocatorKey and BuildConfig.yandexMapKey ( [instruction](https://stackoverflow.com/questions/35722904/saving-the-api-key-in-gradle-properties) ). 40 | 41 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | buildToolsVersion = '35.0.0' 5 | defaultConfig { 6 | applicationId "truewatcher.tower" 7 | minSdkVersion 21 8 | targetSdkVersion 34 9 | compileSdk 34 10 | } 11 | buildTypes { 12 | release { 13 | // no optimisation 14 | minifyEnabled false 15 | 16 | // optimisation 17 | //minifyEnabled true 18 | //shrinkResources true 19 | //proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 20 | 21 | // optimise more agressively 22 | //proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.txt' 23 | } 24 | } 25 | namespace 'truewatcher.tower' 26 | buildFeatures { 27 | buildConfig true 28 | } 29 | } 30 | 31 | dependencies { 32 | //implementation 'com.android.support:appcompat-v7:28.0.0' 33 | //implementation 'com.android.support:preference-v7:28.0.0' 34 | implementation 'androidx.appcompat:appcompat:1.7.0' 35 | implementation "androidx.preference:preference:1.2.1" 36 | // cures "duplicate classes" error 37 | implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/assets/icons_readme/ic_tower1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/assets/webMaps/leafletjs_1_9/images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrueWatcher/tower/6699f23ed40de1991d5b2304d99e40747054d5e3/app/src/main/assets/webMaps/leafletjs_1_9/images/layers-2x.png -------------------------------------------------------------------------------- /app/src/main/assets/webMaps/leafletjs_1_9/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrueWatcher/tower/6699f23ed40de1991d5b2304d99e40747054d5e3/app/src/main/assets/webMaps/leafletjs_1_9/images/layers.png -------------------------------------------------------------------------------- /app/src/main/assets/webMaps/leafletjs_1_9/images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrueWatcher/tower/6699f23ed40de1991d5b2304d99e40747054d5e3/app/src/main/assets/webMaps/leafletjs_1_9/images/marker-icon-2x.png -------------------------------------------------------------------------------- /app/src/main/assets/webMaps/leafletjs_1_9/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrueWatcher/tower/6699f23ed40de1991d5b2304d99e40747054d5e3/app/src/main/assets/webMaps/leafletjs_1_9/images/marker-icon.png -------------------------------------------------------------------------------- /app/src/main/assets/webMaps/leafletjs_1_9/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrueWatcher/tower/6699f23ed40de1991d5b2304d99e40747054d5e3/app/src/main/assets/webMaps/leafletjs_1_9/images/marker-shadow.png -------------------------------------------------------------------------------- /app/src/main/assets/webMaps/wallpaper.css: -------------------------------------------------------------------------------- 1 | 2 | html, body, #mapDiv, table { 3 | width: 100%; height: 100%; margin: 0; padding: 0; border: 0; 4 | } 5 | 6 | #mapDiv td { 7 | vertical-align: middle; text-align: center; 8 | } 9 | 10 | #svg_logo { 11 | min-width: 20%; min-height: 20%; 12 | } 13 | 14 | #mapDiv { 15 | background-image: linear-gradient(rgba(255,255,255,0), #a0a0a0, #aaa, #592); 16 | } 17 | @media (prefers-color-scheme: dark) { 18 | #mapDiv { 19 | background-image: linear-gradient(#555, #a0a0a0, #aaa, #592); 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/assets/webMaps/wallpaper.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wallpaper 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 |
14 | 15 |
17 | 18 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher3-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrueWatcher/tower/6699f23ed40de1991d5b2304d99e40747054d5e3/app/src/main/ic_launcher3-web.png -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/CellPointFetcher.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | import android.Manifest; 6 | import android.os.AsyncTask; 7 | import android.os.Build; 8 | //import android.support.annotation.RequiresApi; 9 | import androidx.annotation.RequiresApi; 10 | import android.util.Log; 11 | import androidx.fragment.app.FragmentActivity; 12 | 13 | public class CellPointFetcher extends PointFetcher implements PermissionReceiver,CellDataReceiver,HttpReceiver { 14 | 15 | private CellResolver mCellResolver; 16 | private CellInformer mCellInformer = new CellInformer(); 17 | 18 | @Override 19 | protected String getPermissionType() { return Manifest.permission.ACCESS_FINE_LOCATION; } 20 | 21 | @Override 22 | protected int getPermissionCode() { return 1; } 23 | 24 | @RequiresApi(api = Build.VERSION_CODES.Q) 25 | @Override 26 | public void afterLocationPermissionOk() { 27 | mCellInformer.bindActivity(mActivity); 28 | mCellInformer.requestCellInfos(this); 29 | } 30 | 31 | public void onCellDataObtained(JSONObject cellData) { 32 | mStatus = mCellInformer.getStatus(); 33 | if (mStatus.equals("forbidden")) { 34 | mPi.addProgress(mActivity.getResources().getString(R.string.permissionfailure)); 35 | return; 36 | } 37 | if (mStatus.indexOf("unsupported") == 0) { 38 | mPi.addProgress(mStatus); 39 | return; 40 | } 41 | if (! mStatus.equals("available") && ! mStatus.equals("mocking") && ! mStatus.equals("noService")) { 42 | mStatus="error"; 43 | mPi.addProgress("failed to get cell info"); 44 | return; 45 | } 46 | 47 | mPoint=new Point("cell"); 48 | mPoint.cellData=cellData.toString(); 49 | mPi.showData( JsonHelper.filterQuotes(mPoint.cellData) ); 50 | if (shouldNotResolve()) { 51 | onPointavailable(mPoint); 52 | return; 53 | } 54 | mPi.addProgress("trying to get location..."); 55 | startResolveCell(); 56 | } 57 | 58 | private boolean shouldNotResolve() { 59 | if ("none".equals( MyRegistry.getInstance().get("cellResolver") ) ) { 60 | mPi.addProgress("location service is off (see Settings)"); 61 | return true; 62 | } 63 | //if (U.DEBUG) Log.i(U.TAG,"network on:"+U.isNetworkOn(mActivity)); 64 | if (! U.isNetworkOn(mActivity)) { 65 | mPi.addProgress("no internet"); 66 | return true; 67 | } 68 | return false; 69 | } 70 | 71 | public void onlyResolve(PointIndicator pi, PointReceiver pr, Point p) { 72 | // a shortcut entrypoint for resolving a ready cell without permission checks 73 | mPi=pi; 74 | mPointReceiver=pr; 75 | mPoint=p; 76 | mStatus="asked to resolve"; 77 | mToUpdateLocation=false; 78 | if ( ! p.getType().equals("cell") || p.cellData == null || p.cellData.length() < 10) { 79 | mPi.addProgress("Not a valid cell"); 80 | return; 81 | } 82 | if ( "none".equals( MyRegistry.getInstance().get("cellResolver") ) ) { 83 | mPi.addProgress("Select cell location service"); 84 | return; 85 | } 86 | startResolveCell(); 87 | } 88 | 89 | private void startResolveCell() { 90 | String resolverUri = ""; 91 | String reqData = ""; 92 | JSONObject cellData = new JSONObject(); 93 | 94 | try { 95 | cellData = new JSONObject(mPoint.cellData); 96 | } 97 | catch (JSONException e) { 98 | Log.e(U.TAG,"Wrong point.cellData"); 99 | Log.e(U.TAG,e.getMessage()); 100 | mPi.addProgress("Wrong point.cellData"); 101 | onPointavailable(mPoint); 102 | return; 103 | } 104 | try { 105 | if ( ! cellData.has("CID") || "".equals(cellData.optString("CID"))) { 106 | throw new U.DataException("No cell id"); 107 | } 108 | mCellResolver = CellResolverFactory.getResolver(MyRegistry.getInstance().get("cellResolver")); 109 | resolverUri = mCellResolver.makeResolverUri(cellData); 110 | reqData = mCellResolver.makeResolverData(cellData); 111 | } 112 | catch (U.DataException e) { 113 | Log.e(U.TAG,e.getMessage()); 114 | mStatus="Failure:"+e.getMessage(); 115 | mPi.addProgress(mStatus); 116 | onPointavailable(mPoint); 117 | return; 118 | } 119 | if (U.DEBUG) Log.i(U.TAG,"startResolveCell:"+"About to query "+resolverUri+"\n data="+reqData); 120 | AsyncTask req = mCellResolver.getRequestTask(this); 121 | req.execute(resolverUri,reqData); 122 | } 123 | 124 | @Override 125 | public void onHttpReceived(String response) { 126 | if (U.DEBUG) Log.i(U.TAG,"onHttpReceived:"+"Response:"+response); 127 | try { 128 | JSONObject resolvedData = mCellResolver.getResolvedData(response); 129 | mStatus = "resolved"; 130 | mPoint.lat=resolvedData.optString("lat"); 131 | mPoint.lon=resolvedData.optString("lon"); 132 | String r=resolvedData.optString("range"); 133 | if ( r != null && ! r.isEmpty()) mPoint.range=r; 134 | } 135 | catch (U.DataException e) { 136 | Log.e(U.TAG,e.getMessage()); 137 | mStatus="Failure:"+e.getMessage(); 138 | } 139 | 140 | mPi.addProgress(mStatus); 141 | //if (mStatus != "resolved") return; 142 | if (mPoint.range != null && ! mPoint.range.isEmpty()) { 143 | String rt=PointIndicator.floor(mPoint.range); 144 | mPi.addData(" Accuracy:"+rt); 145 | } 146 | onPointavailable(mPoint); 147 | } 148 | 149 | @Override 150 | public void onHttpError(String error) { 151 | mStatus="Http Error:"+error; 152 | mPi.addProgress(mStatus); 153 | onPointavailable(mPoint); 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/CellResolverFactory.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | 6 | import android.net.Uri; 7 | import android.os.AsyncTask; 8 | import android.util.Log; 9 | 10 | interface CellResolver { 11 | public String makeResolverUri(JSONObject celldata); 12 | public String makeResolverData(JSONObject celldata) throws U.DataException; 13 | public AsyncTask getRequestTask(HttpReceiver receiver); 14 | public JSONObject getResolvedData(String response) throws U.DataException; 15 | } 16 | 17 | public class CellResolverFactory { 18 | public static CellResolver getResolver(String which) { 19 | if (which.equals("mylnikov")) return new MylnikovResolver(); 20 | if (which.equals("yandex")) return new YandexResolver(); 21 | throw new U.RunException("CellInformer:"+"Wrong WHICH="+which); 22 | } 23 | 24 | private static class MylnikovResolver implements CellResolver { 25 | 26 | public String makeResolverUri(JSONObject cellData) { 27 | String resolverServiceBase="api.mylnikov.org/geolocation/cell"; 28 | String lacTac = cellData.has("TAC" ) ? "TAC" : "LAC"; 29 | // https://api.mylnikov.org/geolocation/cell?v=1.1&mcc=250&mnc=02&cellid=200719106&lac=7840 30 | String resolverUri = Uri.parse(U.H+resolverServiceBase) 31 | .buildUpon() 32 | .appendQueryParameter("v", "1.1") 33 | //.appendQueryParameter("data", "open") 34 | .appendQueryParameter("mcc", cellData.optString("MCC")) 35 | .appendQueryParameter("mnc", cellData.optString("MNC")) 36 | .appendQueryParameter("lac", cellData.optString(lacTac)) 37 | .appendQueryParameter("cellid", cellData.optString("CID")) 38 | .build().toString(); 39 | return resolverUri; 40 | } 41 | 42 | public String makeResolverData(JSONObject celldata) { return ""; } 43 | 44 | public AsyncTask getRequestTask(HttpReceiver receiver) { 45 | return new HttpGetRequest(receiver); 46 | } 47 | 48 | public JSONObject getResolvedData(String response) throws U.DataException { 49 | JSONObject rd=new JSONObject(); 50 | if (response == null || response.length() == 0) { throw new U.DataException("No response"); } 51 | try { 52 | rd=new JSONObject(response); 53 | int respCode=rd.optInt("result"); 54 | if (respCode != 200) { throw new U.DataException("Resolver failure, code="+respCode); } 55 | rd=rd.getJSONObject("data"); 56 | } 57 | catch (JSONException e) { throw new U.DataException("Unparseble response"); } 58 | String lat=String.valueOf(rd.opt("lat")); 59 | String lon=String.valueOf(rd.opt("lon")); 60 | if (lat.indexOf(".") < 0 || lon.indexOf(".") < 0) { 61 | throw new U.DataException("Wrong lat or lon:"+lat+"/"+lon); 62 | } 63 | return rd; 64 | } 65 | } 66 | 67 | private static class YandexResolver implements CellResolver { 68 | 69 | public String makeResolverUri(JSONObject cellData) { 70 | return U.H+"api.lbs.yandex.net/geolocation"; 71 | } 72 | 73 | public String makeResolverData(JSONObject cellData) throws U.DataException { 74 | String key=MyRegistry.getInstance().getScrambled("yandexLocatorKey"); 75 | //Log.d(U.TAG, "CellResolverFactory"+"locator key:"+MyRegistry.getInstance().getScrambled("yandexLocatorKey")); 76 | if (key.isEmpty()) { throw new U.DataException("missing API key"); } 77 | String r="json={"; 78 | r+="\"common\":{\"version\":\"1.0\", \"api_key\":\""+key+"\"}"; 79 | r+=", "; 80 | JSONObject data=new JSONObject(); 81 | String lacTac = cellData.has("TAC" ) ? "TAC" : "LAC"; 82 | try { 83 | data.put("countrycode",cellData.optInt("MCC")); 84 | data.put("operatorid",cellData.optInt("MNC")); 85 | data.put("lac",cellData.optLong(lacTac)); 86 | data.put("cellid",cellData.optLong("CID")); 87 | } 88 | catch (JSONException e) { 89 | Log.e(U.TAG, "makeResolverData:"+e.getMessage()); 90 | } 91 | r+="\"gsm_cells\":["+data.toString()+"]"; 92 | r+="}"; 93 | return r; 94 | } 95 | 96 | public AsyncTask getRequestTask(HttpReceiver receiver) { 97 | return new HttpPostRequest(receiver); 98 | } 99 | 100 | public JSONObject getResolvedData(String response) throws U.DataException { 101 | JSONObject rd=new JSONObject(); 102 | JSONObject res=new JSONObject(); 103 | if (response == null || response.length() == 0) { throw new U.DataException("No response"); } 104 | try { 105 | rd=new JSONObject(response); 106 | String error=rd.optString("error"); 107 | if (error != null && ! error.isEmpty()) { throw new U.DataException("Resolver failure, error="+error); } 108 | rd=rd.getJSONObject("position"); 109 | } 110 | catch (JSONException e) { throw new U.DataException("Unparseble response"); } 111 | String lat=String.valueOf(rd.opt("latitude")); 112 | String lon=String.valueOf(rd.opt("longitude")); 113 | if (lat.indexOf(".") < 0 || lon.indexOf(".") < 0) { 114 | throw new U.DataException("Wrong lat or lon:"+lat+"/"+lon); 115 | } 116 | String range=String.valueOf(rd.opt("precision")); 117 | try { 118 | res.put("lat",lat); 119 | res.put("lon",lon); 120 | if ( range != null && ! range.isEmpty()) res.put("range",range); 121 | } 122 | catch (JSONException e) { throw new U.DataException("Unrecodable response"); } 123 | return res; 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/ConfirmationDialogFragment.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Dialog; 5 | //import android.support.v4.app.DialogFragment; 6 | import androidx.appcompat.app.AppCompatDialogFragment; 7 | import android.content.Context; 8 | import android.content.DialogInterface; 9 | import android.os.Bundle; 10 | //import android.support.v7.app.AlertDialog; 11 | import androidx.appcompat.app.AlertDialog; 12 | 13 | public class ConfirmationDialogFragment extends AppCompatDialogFragment { 14 | // https://stackoverflow.com/questions/5393197/show-dialog-from-fragment -- answer by EpicPandaForce 15 | public interface ConfirmationDialogReceiver { 16 | public void onConfirmationPositive(int id);//DialogFragment dialog 17 | public void onConfirmationNegative(int id); 18 | } 19 | 20 | private ConfirmationDialogReceiver mListener; 21 | private AppCompatDialogFragment mMe=this; 22 | private int mId=0; 23 | private int mStringId=0; 24 | 25 | public ConfirmationDialogFragment() { super(); } 26 | 27 | @TargetApi(23) 28 | @Override 29 | public void onAttach(Context context) { 30 | super.onAttach(context); 31 | try { 32 | //mListener = (ConfirmationDialogReceiver) context; 33 | mListener = (ConfirmationDialogReceiver) getTargetFragment(); 34 | } 35 | catch (ClassCastException e) { 36 | throw new ClassCastException("Host activity must implement ConfirmationDialogReceiver"); 37 | } 38 | } 39 | 40 | @Override 41 | public Dialog onCreateDialog(Bundle savedInstanceState) { 42 | mId=Integer.valueOf(this.getArguments().getString("actionId")); 43 | mStringId=Integer.valueOf(this.getArguments().getString("actionStringId")); 44 | //Log.i(U.TAG, "ConfirmationDialogFragment:"+"got args="+mId+"/"+ mStringId); 45 | if (mId == 0 || mStringId == 0) throw new U.RunException("ConfirmationDialogFragment:Missing arguments"); 46 | 47 | AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 48 | builder 49 | .setMessage(R.string.are_you_sure) 50 | .setPositiveButton(mStringId, new DialogInterface.OnClickListener() { 51 | public void onClick(DialogInterface dialog, int id) { 52 | mListener.onConfirmationPositive(mId); 53 | } 54 | }) 55 | .setNegativeButton(R.string.action_cancel, new DialogInterface.OnClickListener() { 56 | public void onClick(DialogInterface dialog, int id) { 57 | mListener.onConfirmationNegative(mId); 58 | } 59 | }); 60 | return builder.create(); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/DeeperRadioGroup.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.view.View.OnClickListener; 9 | import android.widget.RadioButton; 10 | 11 | public class DeeperRadioGroup { 12 | 13 | private int mCheckedRadio=-1; 14 | 15 | public DeeperRadioGroup(ViewGroup radioContainer) { 16 | setRadioExclusiveClick(radioContainer); 17 | } 18 | 19 | public int getCheckedRadioButtonId() { return mCheckedRadio; } 20 | 21 | // https://stackoverflow.com/questions/10461005/how-to-group-radiobutton-from-different-linearlayouts 22 | private void setRadioExclusiveClick(ViewGroup parent) { 23 | final List radios = getRadioButtons(parent); 24 | 25 | for (RadioButton radio: radios) { 26 | radio.setOnClickListener(new OnClickListener() { 27 | @Override 28 | public void onClick(View v) { 29 | RadioButton r = (RadioButton) v; 30 | r.setChecked(true); 31 | mCheckedRadio=r.getId(); 32 | for (RadioButton r2:radios) { if (r2.getId() != r.getId()) r2.setChecked(false); } 33 | } 34 | }); 35 | } 36 | } 37 | 38 | private List getRadioButtons(ViewGroup parent) { 39 | List radios = new ArrayList(); 40 | for (int i=0;i < parent.getChildCount(); i++) { 41 | View v = parent.getChildAt(i); 42 | if (v instanceof RadioButton) { 43 | radios.add((RadioButton) v); 44 | if (((RadioButton)v).isChecked()) mCheckedRadio=v.getId(); 45 | } 46 | else if (v instanceof ViewGroup) { 47 | List nestedRadios = getRadioButtons((ViewGroup) v); 48 | radios.addAll(nestedRadios); 49 | } 50 | } 51 | return radios; 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/EditTextDialogFragment.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Dialog; 5 | //import android.support.v4.app.DialogFragment; 6 | import androidx.appcompat.app.AppCompatDialogFragment; 7 | import android.content.Context; 8 | import android.content.DialogInterface; 9 | import android.os.Bundle; 10 | //import android.support.v7.app.AlertDialog; 11 | import androidx.appcompat.app.AlertDialog; 12 | import android.util.Log; 13 | import android.view.LayoutInflater; 14 | import android.view.View; 15 | import android.view.ViewGroup; 16 | import android.widget.EditText; 17 | import android.widget.LinearLayout; 18 | 19 | public class EditTextDialogFragment extends AppCompatDialogFragment { 20 | // https://stackoverflow.com/questions/5393197/show-dialog-from-fragment -- answer by EpicPandaForce 21 | public interface EditTextDialogReceiver { 22 | public void onEditTextPositive(int id, String text); 23 | } 24 | 25 | private EditTextDialogReceiver mListener; 26 | private AppCompatDialogFragment mMe=this; 27 | private int mId=0; 28 | private int mStringId=0; 29 | private String mText=""; 30 | private EditText mInput; 31 | 32 | public EditTextDialogFragment() { super(); } 33 | 34 | @TargetApi(23) 35 | @Override 36 | public void onAttach(Context context) { 37 | super.onAttach(context); 38 | try { 39 | //mListener = (ConfirmationDialogReceiver) context; 40 | mListener = (EditTextDialogReceiver) getTargetFragment(); 41 | } 42 | catch (ClassCastException e) { 43 | throw new ClassCastException("Host activity must implement EditTextDialogReceiver"); 44 | } 45 | } 46 | 47 | @Override 48 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 49 | View v=super.onCreateView(inflater,container, savedInstanceState); 50 | return v; 51 | } 52 | 53 | @Override 54 | public Dialog onCreateDialog(Bundle savedInstanceState) { 55 | mId=Integer.valueOf(this.getArguments().getString("actionId")); 56 | mStringId=Integer.valueOf(this.getArguments().getString("actionStringId")); 57 | mText=this.getArguments().getString("text"); 58 | if (mId == 0 || mStringId == 0 || mText == null) throw new U.RunException("EditTextDialogFragment:Missing arguments"); 59 | 60 | AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 61 | builder.setMessage(mStringId); 62 | // https://stackoverflow.com/questions/18799216/how-to-make-a-edittext-box-in-a-dialog 63 | mInput = new EditText(getActivity()); 64 | LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 65 | LinearLayout.LayoutParams.MATCH_PARENT, 66 | LinearLayout.LayoutParams.MATCH_PARENT); 67 | mInput.setLayoutParams(lp); 68 | if (mText.isEmpty()) mInput.setHint(""); 69 | mInput.setText(mText); 70 | builder.setView(mInput); 71 | 72 | builder 73 | .setPositiveButton(R.string.action_done, new DialogInterface.OnClickListener() { 74 | public void onClick(DialogInterface dialog, int id) { 75 | mText=mInput.getText().toString(); 76 | if (U.DEBUG) Log.d(U.TAG,"EditTextDialogFragment:"+"Action confirmed by user, text="+mText); 77 | mListener.onEditTextPositive(mId, mText); 78 | } 79 | }) 80 | .setNegativeButton(R.string.action_cancel, new DialogInterface.OnClickListener() { 81 | public void onClick(DialogInterface dialog, int id) { 82 | //mListener.onConfirmationNegative(mId); 83 | if (U.DEBUG) Log.d(U.TAG,"EditTextDialogFragment:"+"Action canceled by user"); 84 | } 85 | }); 86 | return builder.create(); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/ForegroundService.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import android.app.Notification; 4 | import android.app.NotificationChannel; 5 | import android.app.NotificationManager; 6 | import android.app.PendingIntent; 7 | import android.app.Service; 8 | import android.content.Intent; 9 | import android.content.pm.ServiceInfo; 10 | import android.os.Build; 11 | import android.os.IBinder; 12 | import androidx.annotation.Nullable; 13 | import androidx.annotation.RequiresApi; 14 | //import android.support.v4.app.NotificationCompat; 15 | import androidx.core.app.NotificationCompat; 16 | import android.util.Log; 17 | 18 | // https://www.here.com/docs/bundle/sdk-for-android-navigate-developer-guide/page/topics/get-locations-enable-background-updates.html 19 | 20 | public class ForegroundService extends Service { 21 | 22 | private static final int NOTIFICATION_ID = 12345678; 23 | private static final String CHANNEL_ID = "channel_01"; 24 | 25 | @Override 26 | public void onCreate() { 27 | super.onCreate(); 28 | if (U.DEBUG) Log.i(U.TAG, "ForegroundService:"+"onCreate"); 29 | } 30 | 31 | @RequiresApi(api = Build.VERSION_CODES.Q) 32 | @Override 33 | public int onStartCommand(Intent intent, int flags, int startId) { 34 | if (U.DEBUG) Log.i(U.TAG, "ForegroundService:"+"onStartCommand"); 35 | Model.getInstance().getTrackStorage().saveNote( 36 | "onStartCommand", 37 | String.format("intnt:%b,flags:%d,id:%d",intent,flags,startId)); 38 | createNotificationChannel(); 39 | try { 40 | startForeground(NOTIFICATION_ID, getNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION); 41 | } 42 | catch (NoSuchMethodError e) { // API <= 28 43 | startForeground(NOTIFICATION_ID, getNotification()); 44 | } 45 | if (U.DEBUG) Log.i(U.TAG, "ForegroundService:"+"onStartCommand"+": service started!"); 46 | return START_STICKY; 47 | } 48 | 49 | private Notification getNotification() { 50 | String text="tracking is on"; 51 | Intent intent = new Intent(this, MainActivity.class); 52 | PendingIntent contentIntent = PendingIntent.getActivity( 53 | this, 54 | 0, 55 | intent, 56 | PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT ); 57 | 58 | NotificationCompat.Builder builder = new NotificationCompat.Builder(this) 59 | .setContentTitle("Tower") 60 | .setContentText(text) 61 | .setSmallIcon(R.mipmap.ic_launcher3) 62 | .setContentIntent(contentIntent) 63 | ; 64 | 65 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 66 | builder.setChannelId(CHANNEL_ID); 67 | } 68 | 69 | return builder.build(); 70 | } 71 | 72 | private void createNotificationChannel() { 73 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 74 | NotificationChannel serviceChannel = new NotificationChannel( 75 | CHANNEL_ID, 76 | "Foreground Service Channel", 77 | NotificationManager.IMPORTANCE_DEFAULT 78 | ); 79 | 80 | NotificationManager manager = getSystemService(NotificationManager.class); 81 | manager.createNotificationChannel(serviceChannel); 82 | } 83 | } 84 | 85 | @Override 86 | public void onDestroy() { 87 | super.onDestroy(); 88 | } 89 | 90 | @Nullable 91 | @Override 92 | public IBinder onBind(Intent intent) { 93 | return null; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/GpsPointFetcher.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import android.Manifest; 4 | import android.content.Context; 5 | import android.location.Location; 6 | import android.location.LocationListener; 7 | import android.location.LocationManager; 8 | import android.os.Build; 9 | import android.os.Bundle; 10 | import android.os.SystemClock; 11 | import android.util.Log; 12 | 13 | public class GpsPointFetcher extends PointFetcher { 14 | 15 | private LocationManager mLocnManager; 16 | private int mFixCount=0; 17 | private int mGpsAcceptableAccuracy=MyRegistry.getInstance().getInt("gpsAcceptableAccuracy"); 18 | private int mGpsMaxFixCount=MyRegistry.getInstance().getInt("gpsMaxFixCount"); 19 | 20 | @Override 21 | protected String getPermissionType() { return Manifest.permission.ACCESS_FINE_LOCATION; } 22 | 23 | @Override 24 | protected int getPermissionCode() { return 2; } 25 | 26 | /* mock gps location seems to be off limits now :( 27 | @Override 28 | protected boolean tryGiveMockLocation() { 29 | if (mStatus.equals("enabled")) { 30 | giveMockLocation(); 31 | return true; 32 | } 33 | return false; 34 | }*/ 35 | 36 | @Override 37 | public void afterLocationPermissionOk() { 38 | mPi.addProgress("checking GPS..."); 39 | mLocnManager = (LocationManager) mActivity.getSystemService(Context.LOCATION_SERVICE); 40 | boolean isGpsEnabled = mLocnManager.isProviderEnabled(LocationManager.GPS_PROVIDER); 41 | if ( ! isGpsEnabled) { 42 | mStatus="disabled"; 43 | mPi.addProgress(mStatus); 44 | return; 45 | } 46 | mStatus="enabled"; 47 | mPi.addProgress("Ok, connecting"); 48 | try { 49 | //the permission is checked in PointFetcher 50 | mLocnManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, mLocationListener); 51 | } 52 | catch (SecurityException e) { 53 | mStatus="forbidden"; 54 | mPi.addProgress((mActivity.getResources().getString(R.string.permissionfailure))); 55 | return; 56 | } 57 | } 58 | 59 | 60 | private LocationListener mLocationListener = new LocationListener() { 61 | 62 | @Override 63 | public void onLocationChanged(Location loc) { 64 | if (U.DEBUG) Log.i(U.TAG, "mLocationListener:"+"got a location " + loc.toString()); 65 | mFixCount += 1; 66 | mPi.showData(locToDisplay(loc, mFixCount)); 67 | mStatus = isAcceptable(loc, mFixCount); 68 | if (!mStatus.equals("overcount") && !mStatus.equals("converged")) return; 69 | mFixCount = 0; 70 | mPoint = new Point("gps", loc); 71 | mLocnManager.removeUpdates(this); 72 | mPi.addProgress(mStatus); 73 | onPointavailable(mPoint); 74 | } 75 | 76 | @Override 77 | public void onStatusChanged(String provider, int status, Bundle extras) { 78 | if ( ! provider.equals(LocationManager.GPS_PROVIDER)) return; 79 | if (U.DEBUG) Log.i(U.TAG, "got new GPS status:"+String.valueOf(status)); 80 | //mPi.showData( "GPS status:" + String.valueOf(status)); 81 | } 82 | 83 | @Override 84 | public void onProviderEnabled(String provider) {} 85 | 86 | @Override 87 | public void onProviderDisabled(String provider) {} 88 | }; 89 | 90 | private String locToDisplay(Location loc, int count) { 91 | String s=""; 92 | s+=String.valueOf(count)+"."; 93 | s+="lat="+U.truncate(loc.getLatitude(), 10)+", lon="+U.truncate(loc.getLongitude(), 10); 94 | if (loc.hasAltitude()) s+=", alt="+U.truncate(loc.getAltitude(), 6); 95 | if (loc.hasAccuracy()) s+=" Accuracy="+String.valueOf(loc.getAccuracy()); 96 | return s; 97 | } 98 | 99 | private String isAcceptable(Location loc, int count) { 100 | String s="not ready"; 101 | if (count >= mGpsMaxFixCount) { s="overcount"; } 102 | else if (loc.hasAccuracy() && loc.getAccuracy() <= mGpsAcceptableAccuracy) { s="converged"; } 103 | return s; 104 | } 105 | 106 | /* 107 | private void giveMockLocation() { 108 | // https://stackoverflow.com/questions/38251741/how-to-set-android-mock-gps-location 109 | 110 | //LocationManager lm = (LocationManager)getSystemService(Context.LOCATION_SERVICE); 111 | //Criteria criteria = new Criteria(); 112 | //criteria.setAccuracy( Criteria.ACCURACY_FINE ); 113 | String mocLocationProvider = LocationManager.GPS_PROVIDER;//lm.getBestProvider( criteria, true ); 114 | 115 | if ( mocLocationProvider == null ) { 116 | mPi.addProgress("No location provider found!"); 117 | return; 118 | } 119 | mLocnManager.addTestProvider(mocLocationProvider, false, false, 120 | false, false, true, true, true, 0, 5); 121 | mLocnManager.setTestProviderEnabled(mocLocationProvider, true); 122 | 123 | Location mockLocation = new Location(mocLocationProvider); // a string 124 | mockLocation.setLatitude(-26.902038); // double 125 | mockLocation.setLongitude(-48.671337); 126 | mockLocation.setAltitude(234.0); 127 | mockLocation.setTime(System.currentTimeMillis()); 128 | mockLocation.setAccuracy(3); 129 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 130 | mockLocation.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos()); 131 | } 132 | mLocnManager.setTestProviderLocation(mocLocationProvider, mockLocation); 133 | if (U.DEBUG) Log.d(U.TAG,"Giving mock gps location"); 134 | }*/ 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/HttpGetRequest.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | import java.net.HttpURLConnection; 7 | import java.net.URL; 8 | import android.os.AsyncTask; 9 | 10 | import javax.net.ssl.HttpsURLConnection; 11 | 12 | interface HttpReceiver { 13 | public void onHttpReceived(String result); 14 | public void onHttpError(String error); 15 | } 16 | 17 | //@link https://medium.com/@JasonCromer/android-asynctask-http-request-tutorial-6b429d833e28 18 | public class HttpGetRequest extends AsyncTask { 19 | public static final String REQUEST_METHOD = "GET"; 20 | public static final int READ_TIMEOUT = 15000; 21 | public static final int CONNECTION_TIMEOUT = 15000; 22 | private HttpReceiver receiver; 23 | 24 | public HttpGetRequest(HttpReceiver rec) { 25 | receiver=rec; 26 | } 27 | 28 | @Override 29 | protected String doInBackground(String... params) { 30 | String stringUrl = params[0]; 31 | String result; 32 | String inputLine; 33 | 34 | try { 35 | //Create a URL object holding our url 36 | URL myUrl = new URL(stringUrl); 37 | 38 | //Create a connection 39 | HttpsURLConnection connection = (HttpsURLConnection) myUrl.openConnection(); 40 | //Set methods and timeouts 41 | connection.setRequestMethod(REQUEST_METHOD); 42 | connection.setReadTimeout(READ_TIMEOUT); 43 | connection.setConnectTimeout(CONNECTION_TIMEOUT); 44 | 45 | //Connect to our url 46 | connection.connect(); 47 | 48 | //Create a new InputStreamReader 49 | InputStreamReader streamReader = new InputStreamReader(connection.getInputStream()); 50 | 51 | //Create a new buffered reader and String Builder 52 | BufferedReader reader = new BufferedReader(streamReader); 53 | StringBuilder stringBuilder = new StringBuilder(); 54 | 55 | //Check if the line we are reading is not null 56 | while ((inputLine = reader.readLine()) != null) { 57 | stringBuilder.append(inputLine); 58 | } 59 | 60 | //Close our InputStream and Buffered reader 61 | reader.close(); 62 | streamReader.close(); 63 | 64 | //Set our result equal to our stringBuilder 65 | result = stringBuilder.toString(); 66 | connection.disconnect(); 67 | } 68 | catch (IOException e) { 69 | e.printStackTrace(); 70 | result = null; 71 | } 72 | 73 | return result; 74 | } 75 | 76 | protected void onPostExecute(String result){ 77 | super.onPostExecute(result); 78 | receiver.onHttpReceived(result); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/HttpPostRequest.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.BufferedWriter; 5 | import java.io.InputStreamReader; 6 | import java.io.OutputStream; 7 | import java.io.OutputStreamWriter; 8 | import java.io.UnsupportedEncodingException; 9 | import java.net.HttpURLConnection; 10 | import java.net.URL; 11 | import java.net.URLEncoder; 12 | import java.util.Map; 13 | 14 | import android.os.AsyncTask; 15 | import android.util.Log; 16 | 17 | import javax.net.ssl.HttpsURLConnection; 18 | 19 | //@link https://stackoverflow.com/questions/9767952/how-to-add-parameters-to-httpurlconnection-using-post-using-namevaluepair 20 | public class HttpPostRequest extends AsyncTask { 21 | private HttpReceiver receiver; 22 | 23 | public HttpPostRequest(HttpReceiver rec) { 24 | receiver=rec; 25 | } 26 | public static String makePostDataString( Map params ) throws UnsupportedEncodingException{ 27 | StringBuilder result = new StringBuilder(); 28 | boolean first = true; 29 | for (Map.Entry entry : params.entrySet()){ 30 | if (first) { first = false; } 31 | else { result.append("&"); } 32 | result.append(URLEncoder.encode(entry.getKey(), "UTF-8")); 33 | result.append("="); 34 | result.append(URLEncoder.encode(entry.getValue(), "UTF-8")); 35 | } 36 | return result.toString(); 37 | } 38 | 39 | @Override 40 | protected String doInBackground(String... params) { 41 | 42 | String stringUrl = params[0]; 43 | String postDataString = params[1]; 44 | String response = ""; 45 | URL url; 46 | 47 | try { 48 | url = new URL(stringUrl); 49 | HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); 50 | conn.setReadTimeout(15000); 51 | conn.setConnectTimeout(15000); 52 | conn.setRequestMethod("POST"); 53 | conn.setDoInput(true); 54 | conn.setDoOutput(true); 55 | 56 | OutputStream os = conn.getOutputStream(); 57 | BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(os, "UTF-8")); 58 | writer.write(postDataString); 59 | writer.flush(); 60 | writer.close(); 61 | os.close(); 62 | int responseCode=conn.getResponseCode(); 63 | 64 | if (responseCode == HttpURLConnection.HTTP_OK) { 65 | String line; 66 | BufferedReader br=new BufferedReader(new InputStreamReader(conn.getInputStream())); 67 | while ((line=br.readLine()) != null) { response+=line; } 68 | } 69 | else { response=""; } 70 | } 71 | catch (Exception e) { 72 | e.printStackTrace(); 73 | Log.e(U.TAG,"HttpPostRequest:"+e.getMessage()); 74 | } 75 | 76 | return response; 77 | } 78 | 79 | protected void onPostExecute(String result){ 80 | super.onPostExecute(result); 81 | receiver.onHttpReceived(result); 82 | } 83 | 84 | 85 | 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/JSbridge.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import org.json.JSONArray; 4 | import org.json.JSONException; 5 | import android.util.Log; 6 | 7 | public class JSbridge { 8 | private MyRegistry mRegistry=MyRegistry.getInstance(); 9 | private String mZoom; 10 | private String mCenterLon; 11 | private String mCenterLat; 12 | private PointList mPointList; 13 | private int mDirty=3;// load map on first use 14 | private String mViewTrackLatLonJson="[]"; 15 | private String mViewTrackNamesJson="[]"; 16 | private String mCurrentTrackLatLonJson="[]"; 17 | private String mIsBounded=""; 18 | 19 | @android.webkit.JavascriptInterface 20 | public String importLonLat() { return mCenterLon+","+mCenterLat; } 21 | 22 | @android.webkit.JavascriptInterface 23 | public String importLatLon() { return mCenterLat+","+mCenterLon; } 24 | 25 | public void exportLatLon(String lat,String lon) { 26 | mCenterLon=lon; 27 | mCenterLat=lat; 28 | } 29 | 30 | public void exportLatLon(LatLon p) { 31 | if ( ! p.hasCoords()) return; 32 | mCenterLon=p.lon; 33 | mCenterLat=p.lat; 34 | } 35 | 36 | @android.webkit.JavascriptInterface 37 | public void exportCenterLatLon(String lat,String lon) { 38 | mCenterLon=lon; 39 | mCenterLat=lat; 40 | } 41 | 42 | public String importCenterLatLon() { 43 | if (mCenterLat == null || mCenterLon == null) return ""; 44 | return mCenterLat+","+mCenterLon; 45 | } 46 | 47 | @android.webkit.JavascriptInterface 48 | public String importZoom() { 49 | if (mZoom == null) mZoom=importDefaultZoom(); 50 | return mZoom; 51 | } 52 | 53 | @android.webkit.JavascriptInterface 54 | public void saveZoom(String z) { mZoom=z; } 55 | 56 | public void exportZoom(String z) { 57 | mZoom=z; 58 | setDirty(2); 59 | } 60 | 61 | @android.webkit.JavascriptInterface 62 | public String importDefaultZoom() { return mRegistry.get("mapZoom"); } 63 | 64 | @android.webkit.JavascriptInterface 65 | public String importMapType() { 66 | return mRegistry.get("mapProvider"); 67 | } 68 | 69 | @android.webkit.JavascriptInterface 70 | public String getKey() { 71 | boolean isYandex=mRegistry.get("mapProvider").indexOf("yandex") == 0; 72 | if (isYandex) { 73 | //Log.d(U.TAG,"JSbridge:"+"map key:"+MyRegistry.getInstance().getScrambled("yandexMapKey")); 74 | return mRegistry.getScrambled("yandexMapKey"); 75 | } 76 | return ""; 77 | } 78 | 79 | @android.webkit.JavascriptInterface 80 | public String getNamelessMarker() { 81 | Point loc=Model.getInstance().lastPosition; 82 | if (loc == null || ! loc.hasCoords()) return (new JSONArray()).toString(); 83 | return loc.makeJsonPresentation(0).toString(); 84 | } 85 | 86 | @android.webkit.JavascriptInterface 87 | public String importViewTrackLatLonJson() { return mViewTrackLatLonJson; } 88 | 89 | //public void addViewTrackLatLonJson(String json) { 90 | // mViewTrackLatLonJson=U.joinJsonArrays(mViewTrackLatLonJson,json); 91 | // setDirty(2); 92 | //} 93 | 94 | public void pushViewTrack(String json) { 95 | if (json.indexOf(",") >= 0 && ! json.startsWith("[[[")) { 96 | throw new U.RunException("Non-empty and non-3d-array argument"); } 97 | mViewTrackLatLonJson=U.pushJsonArray(mViewTrackLatLonJson,json); 98 | setDirty(2); 99 | } 100 | 101 | public void pushViewTrackName(String name) { 102 | mViewTrackNamesJson=U.joinJsonArrays(mViewTrackNamesJson, "[\"".concat(name).concat("\"]")); 103 | } 104 | 105 | @android.webkit.JavascriptInterface 106 | public String importViewTrackNamesJson() { return mViewTrackNamesJson; } 107 | 108 | @android.webkit.JavascriptInterface 109 | public String importCurrentTrackLatLonJson() { return mCurrentTrackLatLonJson; } 110 | 111 | public void replaceCurrentTrackLatLonJson(String json) { 112 | mCurrentTrackLatLonJson=json; 113 | setDirty(1); 114 | } 115 | 116 | public void consumeLocation(Point p) { 117 | if (p == null || ! p.hasCoords()) return; 118 | exportLatLon(p.lat,p.lon); 119 | setDirty(2); 120 | } 121 | 122 | public void consumeTrackpoint(Trackpoint p) { 123 | if ( ! mRegistry.getBool("enableTrack")) return; 124 | if (p == null || ! p.getType().equals("T")) return; 125 | String ll=p.makeJsonPresentation().toString(); 126 | if (U.DEBUG) Log.d(U.TAG, "consumeTrackpoint:" + "Adding:" + ll); 127 | mCurrentTrackLatLonJson = StorageHelper.append2LatLonString(ll, p.isNewSegment(), mCurrentTrackLatLonJson); 128 | //Log.d(U.TAG, "consumeTrackpoint:" + "Result:" + mCurrentTrackLatLonJson); 129 | if (mRegistry.getBool("shouldCenterMapOnTrack")) { 130 | exportLatLon(p); 131 | } 132 | setDirty(1); 133 | } 134 | 135 | @android.webkit.JavascriptInterface 136 | public boolean importViewCurrentTrack() { return mRegistry.getBool("enableTrack"); } 137 | 138 | @android.webkit.JavascriptInterface 139 | public boolean importFollowCurrentTrack() { return mRegistry.getBool("shouldCenterMapOnTrack"); } 140 | 141 | @android.webkit.JavascriptInterface 142 | public String getIsBounded() { return mIsBounded; } 143 | 144 | @android.webkit.JavascriptInterface 145 | public void setBounded(String s) { mIsBounded=s; } 146 | 147 | public void setPointList(PointList pl) { mPointList=pl; } 148 | 149 | public void onPoinlistmodified() { setDirty(2); } 150 | 151 | @android.webkit.JavascriptInterface 152 | public String getMarkers() { 153 | return mPointList.makeJsonPresentation(); 154 | } 155 | 156 | public int isDirty() { return mDirty; } 157 | 158 | public void setDirty(int level) { 159 | // 0 - clean, 1 - track, 2 - data, 3 - URI 160 | if (level > mDirty) mDirty=level; 161 | } 162 | public void clearDirty(int level) { 163 | if (level < mDirty) throw new U.RunException("Wrong clearDirty level="+level+", required="+mDirty); 164 | mDirty=0; 165 | } 166 | 167 | public boolean hasNoCenter() { return ( mCenterLat == null || mCenterLat.isEmpty() ); } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/JsonHelper.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | //@link https://gist.githubusercontent.com/codebutler/2339666/raw/f036bc29033bdd6478956f3d3bbeef16acb0ecd3/JsonHelper.java 4 | import org.json.JSONArray; 5 | import org.json.JSONException; 6 | import org.json.JSONObject; 7 | import java.util.*; 8 | 9 | @SuppressWarnings("rawtypes") 10 | public class JsonHelper { 11 | public static Object toJSON(Object object) throws JSONException { 12 | if (object instanceof Map) { 13 | JSONObject json = new JSONObject(); 14 | Map map = (Map) object; 15 | for (Object key : map.keySet()) { 16 | json.put(key.toString(), toJSON(map.get(key))); 17 | } 18 | return json; 19 | } else if (object instanceof Iterable) { 20 | JSONArray json = new JSONArray(); 21 | for (Object value : ((Iterable)object)) { 22 | json.put(value); 23 | } 24 | return json; 25 | } else { 26 | return object; 27 | } 28 | } 29 | 30 | public static boolean isEmptyObject(JSONObject object) { 31 | return object.names() == null; 32 | } 33 | 34 | public static Map getMap(JSONObject object, String key) throws JSONException { 35 | return toMap(object.getJSONObject(key)); 36 | } 37 | 38 | public static Map toMap(JSONObject object) throws JSONException { 39 | Map map = new HashMap(); 40 | Iterator keys = object.keys(); 41 | while (keys.hasNext()) { 42 | String key = (String) keys.next(); 43 | map.put(key, fromJson(object.get(key))); 44 | } 45 | return map; 46 | } 47 | 48 | @SuppressWarnings("unchecked") 49 | public static List toList(JSONArray array) throws JSONException { 50 | List list = new ArrayList(); 51 | for (int i = 0; i < array.length(); i++) { 52 | list.add(fromJson(array.get(i))); 53 | } 54 | return list; 55 | } 56 | 57 | private static Object fromJson(Object json) throws JSONException { 58 | if (json == JSONObject.NULL) { 59 | return null; 60 | } else if (json instanceof JSONObject) { 61 | return toMap((JSONObject) json); 62 | } else if (json instanceof JSONArray) { 63 | return toList((JSONArray) json); 64 | } else { 65 | return json; 66 | } 67 | } 68 | 69 | public static Map toMapSS(JSONObject object) throws JSONException { 70 | Map mapSO=JsonHelper.toMap(object); 71 | Map mapSS = new HashMap(); 72 | for (Map.Entry entry : mapSO.entrySet()) { 73 | mapSS.put( entry.getKey(), Objects.toString(entry.getValue(),"") ); 74 | } 75 | return mapSS; 76 | } 77 | 78 | public static String MapssToJSONString(Map mapSS) { 79 | return new JSONObject(mapSS).toString(); 80 | } 81 | 82 | public static String filterQuotes(String js) { 83 | String r = js.replace("\"",""); 84 | r = r.replaceAll("[{}]",""); 85 | return r; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/LatLon.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | public class LatLon { 4 | public String lat="", lon=""; 5 | 6 | public LatLon() {} 7 | 8 | public LatLon(String aLat, String aLon) { 9 | this.lat=aLat; 10 | this.lon=aLon; 11 | } 12 | 13 | public boolean hasCoords() { 14 | boolean no=( lat == null || lon == null || lat.isEmpty() || lon.isEmpty() ); 15 | return ! no; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/ListHelper.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Map.Entry; 8 | import android.content.Context; 9 | import android.util.Log; 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.ArrayAdapter; 14 | import android.widget.TextView; 15 | 16 | public class ListHelper { 17 | private boolean mToShowProximity=true; 18 | private Map mProximityMap=new HashMap(); 19 | private Map mSortedProximityMap; 20 | private PointList mPointList; 21 | private String mSort="id";// id, rid (id, reverse), pr (proximity) 22 | 23 | public ListHelper(PointList pl) { 24 | mPointList=pl; 25 | } 26 | 27 | public void setSort(String s) { 28 | if (s.equals("pr")) { 29 | mSort=s; 30 | mToShowProximity=true; 31 | } 32 | else if (s.equals("rid")) { mSort=s; } 33 | else mSort="id"; 34 | } 35 | 36 | public ArrayList getList() { 37 | ArrayList nl=new ArrayList(); 38 | Point p; 39 | int l=mPointList.getSize(), i=0; 40 | 41 | if ( ! mPointList.hasProximityOrigin()) { mToShowProximity=false; } 42 | if (mToShowProximity) makeProximityMap(); 43 | if (mSort.equals("pr") && mToShowProximity) { 44 | int key; 45 | sortProximityMap(); 46 | for (Entry entry : mSortedProximityMap.entrySet()) { 47 | key=entry.getKey(); 48 | p=mPointList.getById(key); 49 | nl.add(getPointPresentation(p)); 50 | if (U.DEBUG) Log.d(U.TAG, "ListHelper:"+ "key:"+key+", proximity:"+entry.getValue()); 51 | } 52 | } 53 | else if (mSort.equals("rid")) { 54 | for (i=l-1; i >= 0; i-=1) { nl.add(getPresentation(i)); } 55 | } 56 | else {// sort by id 57 | for (; i < l; i+=1) { nl.add(getPresentation(i)); } 58 | } 59 | return nl; 60 | } 61 | 62 | public int getIdByPosition(int position) { 63 | if (mSort.equals("pr") && mToShowProximity) { 64 | return (new ArrayList(mSortedProximityMap.keySet())).get(position); 65 | } 66 | else if (mSort.equals("rid")) { 67 | return mPointList.getIdByIndex(mPointList.getSize()-position-1); 68 | } 69 | return mPointList.getIdByIndex(position); 70 | } 71 | 72 | private String getPresentation(int position) { 73 | int k=mPointList.getIdByIndex(position); 74 | Point p=mPointList.getById(k); 75 | return getPointPresentation(p); 76 | } 77 | 78 | private void makeProximityMap() { 79 | mProximityMap=new HashMap(); 80 | Point p; 81 | while ((p=mPointList.iterate()) != null) { 82 | mProximityMap.put(p.getId(), U.proximityM(p, mPointList.getProximityOrigin())); 83 | } 84 | mSortedProximityMap=null; 85 | } 86 | 87 | private void sortProximityMap() { 88 | mSortedProximityMap=U.sortByComparator(mProximityMap,true); 89 | } 90 | 91 | public static void printMap(Map map) { 92 | for (Entry entry : map.entrySet()) { 93 | System.out.println("Key : " + entry.getKey() + " Value : "+ entry.getValue()); 94 | } 95 | } 96 | 97 | private String getPointPresentation(Point p) { 98 | String pro=""; 99 | String s; 100 | if (p.isProtected()) pro="🔒 ";// R.string.lock gives an int 101 | s=pro + String.valueOf(p.getId()) + "." + p.getType() + "."+p.getComment(); 102 | if (mToShowProximity) { 103 | s+=" "+proximityToKm(mProximityMap.get(p.getId())); 104 | } 105 | return s; 106 | } 107 | 108 | public String getLocationPresentation() { 109 | if ( ! mPointList.hasProximityOrigin()) { return null; } 110 | Point location=mPointList.getProximityOrigin(); 111 | if ( location.getId() > 0 ) { 112 | return location.getId()+"."+location.getType()+"."+location.getComment(); 113 | } 114 | return location.getType()+"."+location.time; 115 | } 116 | 117 | public static String proximityToKm(double pr) { 118 | if (pr == U.FAR) return "-"; 119 | int km=(int) Math.floor(pr/1000); 120 | if (km == 0) return String.valueOf(Math.round(pr))+"m"; 121 | if (km < 100) return String.valueOf( Math.round(pr/10) / 100d )+"km";// 100d , not 100 !!! 122 | return String.valueOf(km)+"km"; 123 | } 124 | 125 | public boolean getShowProximity() { return mToShowProximity; } 126 | public void setShowProximity() { mToShowProximity=true; } 127 | 128 | public static class MyArrayAdapter extends ArrayAdapter { 129 | private Context mContext; 130 | private List mObjects; 131 | private int mLayoutId; 132 | 133 | public MyArrayAdapter(Context context, int textViewResourceId, List objects) { 134 | super(context, -1, objects); 135 | mContext=context; 136 | mObjects=objects; 137 | mLayoutId=textViewResourceId; 138 | } 139 | 140 | @Override 141 | public View getView(int position, View convertView, ViewGroup parent) { 142 | LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 143 | View rowView = inflater.inflate(mLayoutId, parent, false); 144 | TextView textView = (TextView) rowView.findViewById(R.id.tvItem); 145 | textView.setText(mObjects.get(position)); 146 | return rowView; 147 | } 148 | 149 | public void changeObjects(List objects) { 150 | if (U.DEBUG) Log.d(U.TAG,"MyArrayAdapter:"+"Changing the data list"); 151 | //mObjects=objects; // crashes on point deletion, mObjects must be kept ! 152 | U.refillList(mObjects, objects); 153 | notifyDataSetChanged(); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/MapViewer.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import android.content.Context; 4 | import android.webkit.ConsoleMessage; 5 | import android.webkit.CookieManager; 6 | import android.webkit.WebChromeClient; 7 | import android.webkit.WebStorage; 8 | import android.widget.TextView; 9 | import android.util.Log; 10 | import android.webkit.WebSettings; 11 | import android.webkit.WebView; 12 | import android.webkit.WebViewClient; 13 | 14 | public class MapViewer extends PointIndicator implements PointReceiver { 15 | 16 | private MyRegistry mRegistry=MyRegistry.getInstance(); 17 | private WebView wvWebView; 18 | private String mPageURI; 19 | private JSbridge mJSbridge = Model.getInstance().getJSbridge(); 20 | 21 | public MapViewer(TextView tvP, TextView tvD, WebView wvW) { 22 | super(tvP, tvD); 23 | wvWebView=wvW; 24 | wvWebView.setWebViewClient(new WebViewClient() { 25 | @Override 26 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 27 | twData.setText("Blocked loading "+url); 28 | return true;// false will do loading 29 | } 30 | }); 31 | WebSettings webSettings = wvWebView.getSettings(); 32 | webSettings.setJavaScriptEnabled(true); 33 | webSettings.setAllowContentAccess(false); 34 | webSettings.setAllowFileAccessFromFileURLs(false); 35 | } 36 | 37 | @Override 38 | public void onPointavailable(Point p) { 39 | redraw(); 40 | } 41 | 42 | public void redraw() { 43 | if (mJSbridge.hasNoCenter()) { 44 | if (U.DEBUG) Log.d(U.TAG,"Show wallpaper from redraw"); 45 | showWallpaper(); 46 | return; 47 | } 48 | int c=mJSbridge.isDirty(); 49 | switch (c) { 50 | case 3: 51 | showMap(); 52 | break; 53 | case 2: 54 | reloadData(); 55 | break; 56 | case 1: 57 | reloadTrack(); 58 | break; 59 | case 0: 60 | break; 61 | default: 62 | throw new U.RunException("Unknown JSbribge dirty="+Integer.toString(c)); 63 | } 64 | } 65 | 66 | public void showMap() { 67 | if (mJSbridge.hasNoCenter()) { 68 | if (U.DEBUG) Log.d(U.TAG,"Show wallpaper from showMap"); 69 | showWallpaper(); 70 | return; 71 | } 72 | mPageURI=choosePage(mRegistry.get("mapProvider")); 73 | wvWebView.addJavascriptInterface(mJSbridge, "JSbridge"); 74 | redirectConsole(wvWebView); 75 | wvWebView.loadUrl(mPageURI); 76 | mJSbridge.clearDirty(3); 77 | } 78 | 79 | private String choosePage(String mapProvider) { 80 | final String yandexPage="file:///android_asset/webMaps/ya.html"; 81 | final String leafletjsPage="file:///android_asset/webMaps/leafletjs.html"; 82 | String[] pt=mapProvider.split(" "); 83 | if (pt.length != 2) throw new U.RunException("Wrong mapProvider="+mapProvider); 84 | if ( isLeaflet(pt[0]) ) { return leafletjsPage; } 85 | else if (pt[0].equals("yandex")) { return yandexPage; } 86 | throw new U.RunException("Wrong mapProvider="+mapProvider); 87 | } 88 | 89 | private boolean isLeaflet(String provider) { 90 | final String[] known=new String[] {"osm","opentopo","blank","google"}; 91 | return U.arrayContains(known,provider); 92 | } 93 | 94 | private void showWallpaper() { 95 | final String wallpaperPage="file:///android_asset/webMaps/wallpaper.html"; 96 | wvWebView.loadUrl(wallpaperPage); 97 | } 98 | 99 | private void reloadTrack() { 100 | String reloadTrack="(function() { window.dispatchEvent(onTrackreloadEvent); })();"; 101 | wvWebView.evaluateJavascript(reloadTrack,null); 102 | mJSbridge.clearDirty(1); 103 | } 104 | 105 | private void reloadData() { 106 | String reloadData="(function() { window.dispatchEvent(onDatareloadEvent); })();"; 107 | wvWebView.evaluateJavascript(reloadData,null); 108 | mJSbridge.clearDirty(2); 109 | } 110 | 111 | public void purge(Context context) { 112 | showWallpaper(); 113 | WebStorage.getInstance().deleteAllData(); 114 | CookieManager.getInstance().removeAllCookies(null); 115 | CookieManager.getInstance().flush(); 116 | wvWebView.clearCache(true); 117 | wvWebView.clearFormData(); 118 | wvWebView.clearHistory(); 119 | wvWebView.clearSslPreferences(); 120 | context.deleteDatabase("webview.db"); 121 | context.deleteDatabase("webviewCache.db"); 122 | showMap(); 123 | } 124 | 125 | private void redirectConsole(WebView myWebView) { 126 | if ( ! U.DEBUG) return; 127 | //myWebView.getSettings().setAllowUniversalAccessFromFileURLs(true); 128 | //myWebView.getSettings().setAllowFileAccessFromFileURLs(true); 129 | WebView.setWebContentsDebuggingEnabled(true); 130 | myWebView.setWebChromeClient(new WebChromeClient() { 131 | public boolean onConsoleMessage(ConsoleMessage cm) { 132 | if (U.DEBUG) { 133 | String msg = "WebView console:" + cm.message() + ", line " 134 | + cm.lineNumber() + " of " + cm.sourceId(); 135 | Log.d(U.TAG, msg); 136 | } 137 | return true; 138 | } 139 | }); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/Model.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | public class Model { 7 | private static Model sModel; 8 | public Point lastCell; 9 | public Point lastGps; 10 | public Point lastPosition; 11 | private CellPointFetcher mCellPointFetcher; 12 | private GpsPointFetcher mGpsPointFetcher; 13 | private PointList mPointList; 14 | private StorageHelper mStorageHelper; 15 | private JSbridge mJSbridge; 16 | private TrackStorage mTrackStorage; 17 | private TrackListener mTrackListener; 18 | public CellPointFetcher getCellPointFetcher() { return mCellPointFetcher; } 19 | public GpsPointFetcher getGpsPointFetcher() { return mGpsPointFetcher; } 20 | public PointList getPointList() { return mPointList; } 21 | public StorageHelper getStorageHelper() { return mStorageHelper; } 22 | public JSbridge getJSbridge() { return mJSbridge; } 23 | public TrackStorage getTrackStorage() { return mTrackStorage; } 24 | public TrackListener getTrackListener() { return mTrackListener; } 25 | private boolean mIsFresh=true; 26 | 27 | public static Model getInstance() { 28 | if (sModel == null) { sModel=new Model(); } 29 | return sModel; 30 | } 31 | 32 | private Model() { 33 | mCellPointFetcher = new CellPointFetcher(); 34 | mGpsPointFetcher = new GpsPointFetcher(); 35 | mStorageHelper = new StorageHelper(); 36 | mPointList = new PointList( 0 , mStorageHelper);// must be resized after reading StoredPreferences in MainActivity 37 | mJSbridge = new JSbridge(); 38 | mJSbridge.setPointList(mPointList); 39 | mTrackStorage = new TrackStorage(); 40 | mTrackListener = new TrackListener(mTrackStorage); 41 | } 42 | 43 | public U.Summary[] loadData(Context context, MyRegistry mrg) { 44 | U.Summary[] res = new U.Summary[2]; 45 | res[0] = res[1] = null; 46 | if ( ! mIsFresh) return null; 47 | try { 48 | mPointList.adoptMax(mrg.getInt("maxPoints")); 49 | String targetPath = mStorageHelper.getWorkingFolder(context, mrg); 50 | mStorageHelper.init(targetPath, mrg.get("myFile")); 51 | res[0] = mPointList.load(); 52 | if (U.DEBUG) Log.d(U.TAG,"MainPageFragment:"+ "Loaded "+res[0].adopted+" points"); 53 | 54 | mTrackStorage.initTargetDir(targetPath); 55 | if ( mrg.getBool("enableTrack")) { 56 | TrackStorage.Track2LatLonJSON converter = mTrackStorage.getTrack2LatLonJSON(); 57 | String buf = converter.file2LatLonJSON(); 58 | res[1] = converter.getResults(); 59 | mJSbridge.replaceCurrentTrackLatLonJson(buf); 60 | } 61 | } 62 | catch (Exception e) { 63 | Log.e(U.TAG,"MainPageFragment:"+e.getMessage()); 64 | return null; 65 | } 66 | mIsFresh=false; 67 | return res; 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/MyRegistry.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.os.Build; 6 | import android.preference.PreferenceManager; 7 | import android.util.Log; 8 | 9 | import org.json.JSONException; 10 | import org.json.JSONObject; 11 | 12 | import java.io.IOException; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | // A Singleton registry to keep all parameters 17 | public class MyRegistry { 18 | private static MyRegistry sMe; 19 | private static Map sMap; 20 | private static Context sAppContext=null; 21 | 22 | private MyRegistry() {} 23 | 24 | public static void initMap() throws JSONException { 25 | String d = getDefaultsString(); 26 | Map defaults = JsonHelper.toMapSS(new JSONObject(d)); 27 | sMap=new HashMap(defaults); 28 | } 29 | 30 | public static String toJson() { 31 | return JsonHelper.MapssToJSONString(sMap); 32 | } 33 | 34 | public static MyRegistry getInstance(Context activity) { 35 | sAppContext = activity.getApplicationContext(); 36 | return getInstance(); 37 | } 38 | public static MyRegistry getInstance() { 39 | if(sMe == null) { 40 | sMe=new MyRegistry(); 41 | try { 42 | MyRegistry.initMap(); 43 | } 44 | catch (JSONException e) { 45 | Log.e(U.TAG,"MyRegistry:"+e.toString()); 46 | } 47 | } 48 | return sMe; 49 | } 50 | 51 | public String get(String key) { 52 | if ( ! sMap.keySet().contains(key)) throw new U.RunException("Unknown key="+key); 53 | return sMap.get(key); 54 | } 55 | 56 | public int getInt(String key) { 57 | if ( ! sMap.keySet().contains(key)) throw new U.RunException("Unknown key="+key); 58 | return Integer.parseInt(sMap.get(key)); 59 | } 60 | 61 | public boolean getBool(String key) { 62 | if ( ! sMap.keySet().contains(key)) throw new U.RunException("Unknown key="+key); 63 | return Boolean.parseBoolean(sMap.get(key)); 64 | } 65 | 66 | public String getScrambled(String key) { 67 | int[] transpose = {2,3,5,6,8,9,13,19,25,31}; 68 | char[] ca = sMap.get(key).trim().toCharArray(); 69 | int l=ca.length; 70 | int lHalf=(int) Math.floor(l/2.); 71 | char c; 72 | int sourceIndex,targetIndex; 73 | 74 | if (l < 3) return new String(ca); 75 | for (int i=0; i < transpose.length; i+=1) { 76 | sourceIndex = transpose[i]; 77 | if (sourceIndex >= lHalf) break; 78 | targetIndex = l-sourceIndex; 79 | c=ca[sourceIndex]; 80 | ca[sourceIndex] = ca[targetIndex]; 81 | ca[targetIndex] = c; 82 | } 83 | return new String(ca); 84 | } 85 | 86 | public void set(String key, String val) { 87 | if ( ! sMap.keySet().contains(key)) throw new U.RunException("Unknown key="+key); 88 | sMap.put(key,val); 89 | } 90 | 91 | public void set(String key, boolean val) { 92 | if ( ! sMap.keySet().contains(key)) throw new U.RunException("Unknown key="+key); 93 | sMap.put(key, String.valueOf(val)); 94 | } 95 | 96 | public void set(String key, int val) { 97 | if ( ! sMap.keySet().contains(key)) throw new U.RunException("Unknown key="+key); 98 | sMap.put(key, String.valueOf(val)); 99 | } 100 | 101 | public void set(String key, Object val) { 102 | if ( ! sMap.keySet().contains(key)) throw new U.RunException("Unknown key="+key); 103 | sMap.put(key,val.toString()); 104 | } 105 | 106 | public void setInt(String key, int val) { 107 | if ( ! sMap.keySet().contains(key)) throw new U.RunException("Unknown key="+key); 108 | sMap.put(key, String.valueOf(val)); 109 | } 110 | 111 | public void setBool(String key, boolean val) { 112 | if ( ! sMap.keySet().contains(key)) throw new U.RunException("Unknown key="+key); 113 | sMap.put(key, String.valueOf(val)); 114 | } 115 | 116 | public boolean keyExists(String key) { 117 | return sMap.keySet().contains(key); 118 | } 119 | 120 | private static String getDefaultsString() { 121 | String androidSince11 = Build.VERSION.SDK_INT >= 30 ? "true" : "false"; 122 | 123 | String defs = "{\"cellResolver\":\"none\",\"mapProvider\":\"osm map\",\"mapZoom\":\"17\",\"maxPoints\":\"30\"," 124 | + "\"useTrash\":\"false\",\"gpsAcceptableAccuracy\":\"8\",\"gpsMaxFixCount\":\"10\"," 125 | + "\"myFile\":\"current.csv\"," 126 | + "\"yandexMapKey\":\"\", \"yandexLocatorKey\":\"\", \"isKeylessDistro\":\"false\"," 127 | + "\"gpsMinDistance\":\"12\", \"gpsMinDelayS\":\"10\", \"gpsTimeoutS\":\"120\"," 128 | + "\"enableTrack\":\"true\", \"shouldCenterMapOnTrack\":\"true\", \"useTowerFolder\":\"false\"," 129 | + "\"useMediaFolder\":\""+androidSince11+"\", \"askNotificationPermission\":\"true\"," 130 | + "\"theme\":\"auto\"" 131 | + "}"; 132 | return defs; 133 | } 134 | 135 | public static final String[] INT_KEYS = new String[] { 136 | "mapZoom", "maxPoints", "gpsMinDistance", "gpsMinDelayS" } ; 137 | 138 | public static final String[] APIS = new String[] { "yandexMapKey","yandexLocatorKey" }; 139 | 140 | public void readFromShared() throws U.DataException { 141 | if (sAppContext == null) throw new U.DataException("APPCONTEXT not set"); 142 | String k; 143 | String v; 144 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(sAppContext); 145 | for (Map.Entry e : MyRegistry.sMap.entrySet()) { 146 | k=e.getKey(); 147 | if ( prefs.contains(k) ) { 148 | v = String.valueOf(prefs.getAll().get(k)); 149 | this.set(k, U.enforceInt(MyRegistry.INT_KEYS, k, v)); 150 | } 151 | } 152 | } 153 | 154 | public void saveToShared(String key) { 155 | if ( ! sMap.keySet().contains(key)) throw new U.RunException("Unknown key="+key); 156 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(sAppContext); 157 | SharedPreferences.Editor editor = prefs.edit(); 158 | editor.putString(key, sMap.get(key)).commit(); 159 | } 160 | 161 | private void syncSecret(String key, String assetFileName) { 162 | String s=""; 163 | 164 | if (sMap.get(key).length() > 3) return; // already synced 165 | // look in BuildConfig 166 | if (U.classHasField(BuildConfig.class, key)) { 167 | try { 168 | this.set(key, Class.forName("BuildConfig").getField(key)); 169 | this.set(key, this.getScrambled(key)); 170 | this.saveToShared(key); 171 | if (U.DEBUG) Log.d(U.TAG, "MyRegistry:"+" found a key in buildconfig: "+key); 172 | return; 173 | } 174 | catch (ClassNotFoundException e) { throw new U.RunException("This should not happen 1"); } 175 | catch (NoSuchFieldException e) { throw new U.RunException("This should not happen 2"); } 176 | } 177 | 178 | // look in assets 179 | try { 180 | s=U.readAsset(sAppContext, assetFileName).trim(); 181 | this.set(key,s); 182 | this.saveToShared(key); 183 | } 184 | catch (IOException e) { 185 | if (U.DEBUG) Log.d(U.TAG, "MyRegistry:"+"Missing "+assetFileName); 186 | } 187 | } 188 | 189 | public void syncSecrets() { 190 | syncSecret( "yandexMapKey", "_yandexmap.txt"); 191 | syncSecret( "yandexLocatorKey", "_yandexlocator.txt"); 192 | if (notAllKeys()) { 193 | this.set("isKeylessDistro", true); 194 | this.saveToShared( "isKeylessDistro"); 195 | if (U.DEBUG) Log.d(U.TAG, "MyRegistry:"+"This is a distro without API keys"); 196 | } 197 | } 198 | 199 | public boolean noAnyKeys() { 200 | int i=0; 201 | String k; 202 | for (i=0; i < APIS.length; i+=1) { 203 | if ( ! sMap.get(APIS[i]).isEmpty()) return false; 204 | } 205 | return true; 206 | } 207 | 208 | public boolean notAllKeys() { 209 | int i=0; 210 | String k; 211 | for (i=0; i < APIS.length; i+=1) { 212 | if (sMap.get(APIS[i]).isEmpty()) return true; 213 | } 214 | return false; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/PermissionAwareFragment.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import android.Manifest; 4 | import android.annotation.TargetApi; 5 | import android.content.pm.PackageManager; 6 | import android.os.Build; 7 | //import android.support.annotation.RequiresApi; 8 | import androidx.annotation.RequiresApi; 9 | 10 | import android.util.Log; 11 | import android.util.SparseArray; 12 | 13 | interface PermissionReceiver { 14 | public void receivePermission(int reqCode, boolean isGranted); 15 | } 16 | 17 | public abstract class PermissionAwareFragment extends androidx.fragment.app.Fragment { 18 | private SparseArray mPermissionReceivers = new SparseArray(); 19 | private int mPermissionAttempts = 5; // to break loop on receiving coarse location on SDK .= 31 20 | 21 | 22 | @TargetApi(23) 23 | public void genericRequestPermission(String permCode, int reqCode, PermissionReceiver receiver) { 24 | if (mPermissionAttempts-- > 0) { 25 | mPermissionReceivers.put(reqCode, receiver); 26 | if (U.DEBUG) Log.d(U.TAG, "Requesting user. code=" + reqCode); 27 | requestPermissions(new String[]{permCode}, reqCode); 28 | } 29 | else { 30 | Log.e(U.TAG, "Permission attempts exhausted. code=" + reqCode); 31 | } 32 | } 33 | 34 | @Override 35 | public void onRequestPermissionsResult(int reqCode, String[] permissions, int[] grantResults) { 36 | boolean isGranted = ( grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED ); 37 | if (U.DEBUG) Log.d(U.TAG,"grantResults length="+grantResults.length); 38 | mPermissionReceivers.get(reqCode).receivePermission(reqCode,isGranted); 39 | } 40 | 41 | @RequiresApi(api = Build.VERSION_CODES.M) 42 | protected boolean checkLocationPermission() { 43 | int cl = getActivity().checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION); 44 | if (U.DEBUG) Log.d(U.TAG,"cl="+cl+"/"+PackageManager.PERMISSION_GRANTED); 45 | return (cl == PackageManager.PERMISSION_GRANTED); 46 | } 47 | 48 | protected void askLocationPermission(PermissionReceiver subFragment) { 49 | genericRequestPermission(Manifest.permission.ACCESS_FINE_LOCATION, 1, subFragment); 50 | } 51 | 52 | @RequiresApi(api = Build.VERSION_CODES.M) 53 | protected boolean checkStoragePermission() { 54 | int cl = getActivity().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); 55 | if (U.DEBUG) Log.d(U.TAG,"cl="+cl+"/"+ PackageManager.PERMISSION_GRANTED); 56 | return (cl == PackageManager.PERMISSION_GRANTED); 57 | } 58 | 59 | protected void askStoragePermission(PermissionReceiver subFragment) { 60 | genericRequestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, 2, subFragment); 61 | } 62 | 63 | protected void askNotificationPermission(PermissionReceiver subFragment) { 64 | genericRequestPermission(Manifest.permission.POST_NOTIFICATIONS, 3, subFragment); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/Point.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import java.util.Set; 4 | import org.json.JSONArray; 5 | import android.location.Location; 6 | import android.text.TextUtils; 7 | import java.text.SimpleDateFormat; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Calendar; 11 | import java.util.Collections; 12 | import java.util.HashSet; 13 | import java.util.List; 14 | 15 | public class Point extends LatLon implements Cloneable { 16 | private String mType; 17 | private static final Set TYPES = new HashSet(Arrays.asList( 18 | new String[] {"cell","gps","mark"} 19 | )); 20 | public String alt; 21 | public String range; 22 | public String cellData; 23 | public String time; 24 | private int mId; 25 | private String mComment=""; 26 | private String mNote=""; 27 | private String mSym=""; 28 | private boolean mProtect=false; 29 | public static final List FIELDS = Collections.unmodifiableList(Arrays.asList( 30 | new String[] {"id","type","comment","protect","lat","lon","alt","range","time","cellData","note","sym"} 31 | )); 32 | public static final String SEP = ";"; 33 | public static final String NL = "\n"; 34 | 35 | public Point() { } 36 | 37 | public Point(String t) { 38 | setType(t); 39 | } 40 | 41 | public Point(String t, String lat, String lon) { 42 | setType(t); 43 | this.lat=lat; 44 | this.lon=lon; 45 | } 46 | 47 | public Point(String t, Location loc) { 48 | setType(t); 49 | this.lat=String.valueOf(loc.getLatitude()); 50 | this.lon=String.valueOf(loc.getLongitude()); 51 | if (loc.hasAltitude()) this.alt=String.valueOf(loc.getAltitude()); 52 | if (loc.hasAccuracy()) this.range=String.valueOf(loc.getAccuracy()); 53 | } 54 | 55 | public Object clone() { 56 | try { return super.clone(); } 57 | catch ( CloneNotSupportedException e ) { return null; } 58 | } 59 | 60 | public void setType(String t) { 61 | if ( ! TYPES.contains(t) ) throw new U.RunException("Unhnown type="+t); 62 | mType=t; 63 | } 64 | 65 | public String getType() { return mType; } 66 | 67 | public void setComment(String s) { 68 | int maxChars=20; 69 | if (s.length() > maxChars) s=s.substring(0, maxChars); 70 | mComment=filterChars(s); 71 | } 72 | 73 | public String getNote() { return mNote; } 74 | 75 | public void setNote(String s) { 76 | int maxChars=100; 77 | if (s.length() > maxChars) s=s.substring(0, maxChars); 78 | mNote=filterChars(s); 79 | } 80 | 81 | public String getComment() { return mComment; } 82 | 83 | public void setId(int i) { mId=i; } 84 | public int getId() { return mId; } 85 | 86 | public static String getDate() { 87 | SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm"); 88 | String fd = df.format(Calendar.getInstance().getTime()); 89 | return fd; 90 | } 91 | 92 | public void setCurrentTime() { this.time=Point.getDate(); } 93 | 94 | public boolean isProtected() { return mProtect; } 95 | public void protect() { mProtect=true; } 96 | public void unprotect() { mProtect=false; } 97 | 98 | public JSONArray makeJsonPresentation(int index) { 99 | JSONArray ja=new JSONArray(); 100 | if (index < 0) index=mId; 101 | ja.put(mType); 102 | ja.put(lat); 103 | ja.put(lon); 104 | String text=String.valueOf(index); 105 | if (mComment != null && ! mComment.isEmpty()) text+="."+mComment; 106 | ja.put(text); 107 | return ja;//.toString(); 108 | } 109 | 110 | public String toCsv() { 111 | List ls=new ArrayList(); 112 | ls.add(ne(mId)); 113 | ls.add(ne(mType)); 114 | ls.add(ne(mComment)); 115 | ls.add(ne(mProtect)); 116 | ls.add(ne(lat)); 117 | ls.add(ne(lon)); 118 | ls.add(ne(alt)); 119 | ls.add(ne(range)); 120 | ls.add(ne(time)); 121 | ls.add(ne(cellData)); 122 | ls.add(ne(mNote)); 123 | ls.add(ne(mSym)); 124 | String s= TextUtils.join(Point.SEP, ls); 125 | return s; 126 | } 127 | 128 | public Point fromCsv(String s) throws U.DataException { 129 | s=s.trim(); 130 | String[] ls=TextUtils.split(s, Point.SEP); 131 | if (ls.length != Point.FIELDS.size()) { 132 | throw new U.DataException("Source has "+ls.length+" fields, while "+Point.FIELDS.size()+" are required"); 133 | } 134 | mId=eInt(ls[0]); 135 | if ( ! TYPES.contains(ls[1])) { 136 | throw new U.DataException("Unknown type="+ls[1]+"!"); 137 | } 138 | setType(ls[1]); 139 | setComment(ls[2]); 140 | mProtect=eBool(ls[3]); 141 | lat=ls[4]; 142 | lon=ls[5]; 143 | alt=ls[6]; 144 | range=ls[7]; 145 | time=ls[8]; 146 | cellData=ls[9]; 147 | mNote=ls[10]; 148 | mSym=ls[11]; 149 | return this; 150 | } 151 | 152 | private int eInt(String s) { 153 | if (s.isEmpty()) return 0; 154 | return Integer.valueOf(s); 155 | } 156 | 157 | private boolean eBool(String s) { 158 | if (s.isEmpty() || s.equals("false")) return false; 159 | return true; 160 | } 161 | 162 | private String ne(String s) { 163 | if (s == null || s.isEmpty()) return ""; 164 | if (s.contains(Point.SEP)) throw new U.RunException("Misplaced separator"); 165 | if (s.contains(Point.NL)) throw new U.RunException("Misplaced NL"); 166 | return s; 167 | } 168 | 169 | private String ne(int i) { 170 | if (i <= 0) return ""; 171 | return String.valueOf(i); 172 | } 173 | 174 | private String ne(boolean b) { 175 | if (b) return "true"; 176 | return ""; 177 | } 178 | 179 | public static String filterChars(String s) { 180 | // for a safe export to CSV and GPX 181 | s=s.replace(Point.SEP,","); 182 | s=s.replace(Point.NL,"/"); 183 | s=s.replaceAll("&<>","*"); 184 | return s; 185 | } 186 | 187 | public static String filterCharsMore(String s) { 188 | s=s.replace("\"",""); 189 | s=s.replace(" ","_"); 190 | s=s.replaceAll("[,;&<>~]","*"); 191 | return filterChars(s); 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/PointFetcher.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.pm.PackageManager; 5 | //import android.support.v4.app.FragmentActivity; 6 | import androidx.fragment.app.FragmentActivity; 7 | import android.util.Log; 8 | 9 | interface PointReceiver { 10 | public void onPointavailable(Point p); 11 | } 12 | 13 | public abstract class PointFetcher implements PermissionReceiver { 14 | protected FragmentActivity mActivity; 15 | protected PermissionAwareFragment mFragm; 16 | protected PointIndicator mPi; 17 | protected PointReceiver mPointReceiver; 18 | protected String mStatus="not run"; 19 | protected Point mPoint=null; 20 | protected boolean mToUpdateLocation=true; 21 | 22 | abstract String getPermissionType(); 23 | abstract int getPermissionCode(); 24 | 25 | public String getStatus() { return mStatus; } 26 | 27 | public Point getPoint() { return mPoint; } 28 | 29 | public void setFragment(PermissionAwareFragment f) { 30 | mFragm=f; 31 | mActivity=f.getActivity(); 32 | } 33 | 34 | public void clearFragment() { 35 | mFragm=null; 36 | mActivity=null; 37 | } 38 | 39 | protected boolean tryGiveMockLocation() { return false; } 40 | 41 | public void go(PointIndicator pi, PointReceiver pr) { 42 | mPi=pi; 43 | mPointReceiver=pr; 44 | if (tryGiveMockLocation()) return; 45 | mPi.initProgress(); 46 | //mPi.addProgress("Checking permission..."); 47 | if (U.DEBUG) Log.d(U.TAG, "Checking permission..."); 48 | boolean hasLoctnPerm=checkLocalPermission(); 49 | if ( ! hasLoctnPerm) { 50 | if (U.DEBUG) Log.d(U.TAG, "negative, asking user..."); 51 | //mPi.addProgress("negative, asking user..."); 52 | requestPermission(); 53 | return; 54 | } 55 | if (U.DEBUG) Log.d(U.TAG, "positive"); 56 | //mPi.addProgress("positive"); 57 | afterLocationPermissionOk(); 58 | } 59 | 60 | @TargetApi(23) 61 | private boolean checkLocalPermission() { 62 | if (mActivity == null) { 63 | Log.e(U.TAG, "checkLocalPermission:"+"No activity attached"); 64 | return false; 65 | } 66 | int cl=mActivity.checkSelfPermission(getPermissionType()); 67 | if (U.DEBUG) Log.d(U.TAG,"cl="+cl+"/"+PackageManager.PERMISSION_GRANTED); 68 | return (cl == PackageManager.PERMISSION_GRANTED); 69 | } 70 | 71 | private void requestPermission() { 72 | mFragm.genericRequestPermission(getPermissionType(), getPermissionCode(), this); 73 | } 74 | 75 | public void receivePermission(int reqCode, boolean isGranted) { 76 | if ( ! isGranted) { 77 | if (U.DEBUG) Log.d(U.TAG, "denied"); 78 | mPi.addProgress("denied"); 79 | mStatus="denied"; 80 | return; 81 | } 82 | mPi.addProgress("granted"); 83 | afterLocationPermissionOk(); 84 | } 85 | 86 | abstract void afterLocationPermissionOk(); 87 | 88 | protected void onPointavailable(Point p) { 89 | if ( p.hasCoords() ) mPi.hideProgress();// if it's an unresolved cell - keep progress visible 90 | p.setCurrentTime(); 91 | Model mm = Model.getInstance(); 92 | if (p.getType().equals("cell")) mm.lastCell=p; 93 | else if (p.getType().equals("gps")) mm.lastGps=p; 94 | if (mToUpdateLocation && p.hasCoords()) { 95 | mm.lastPosition=p; 96 | mm.getPointList().setProximityOrigin(p); 97 | mm.getJSbridge().consumeLocation(p); 98 | } 99 | mPointReceiver.onPointavailable(p); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/PointIndicator.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import android.view.View; 4 | import android.widget.TextView; 5 | 6 | public class PointIndicator { 7 | 8 | protected TextView twProgress; 9 | protected TextView twData; 10 | 11 | public PointIndicator(TextView twP, TextView twD) { 12 | twProgress=twP; 13 | twData=twD; 14 | twProgress.setText(""); 15 | twData.setText(""); 16 | } 17 | 18 | public void initProgress() { 19 | twProgress.setText(""); 20 | showProgress(); 21 | } 22 | 23 | public void showProgress(String p) { 24 | twProgress.setText(p); 25 | showProgress(); 26 | } 27 | 28 | public void showProgress() { 29 | twProgress.setVisibility(View.VISIBLE); 30 | } 31 | 32 | public void addProgress(String p) { addProgress(p, ", "); } 33 | 34 | public void addProgress(String p, String separator) { 35 | String t = (String) twProgress.getText(); 36 | if (t.length() > 0) t=t+separator; 37 | twProgress.setVisibility(View.VISIBLE); 38 | twProgress.setText(t+p); 39 | } 40 | 41 | public void hideProgress() { 42 | //twProgress.setText(""); 43 | twProgress.setVisibility(View.GONE); 44 | } 45 | 46 | public void showData(String d) { 47 | twData.setVisibility(View.VISIBLE); 48 | twData.setText(d); 49 | } 50 | 51 | public void addData(String d, String separator) { 52 | twData.setVisibility(View.VISIBLE); 53 | String t = (String) twData.getText(); 54 | if (t.length() > 0) t=t+separator; 55 | twData.setText(t+d); 56 | } 57 | 58 | public void addData(String d) { addData(d, ","); } 59 | 60 | public void hideData() { 61 | //twProgress.setText(""); 62 | twData.setVisibility(View.GONE); 63 | } 64 | 65 | public static String truncate(String s,int max) { 66 | if ( s.length() > max ) return s.substring(0,max); 67 | return s; 68 | } 69 | 70 | public static String floor(String s) { 71 | if (s == null) return ""; 72 | if (s.isEmpty()) return s; 73 | String r=s; 74 | int p=s.indexOf("."); 75 | if (p > 0) r=r.substring(0,p); 76 | return r; 77 | } 78 | 79 | public void clearIndicator() { 80 | twProgress.setText(""); 81 | twData.setText(""); 82 | } 83 | 84 | public void hideIndicator() { 85 | hideProgress(); 86 | hideData(); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/PointList.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import org.json.JSONArray; 7 | import android.util.Log; 8 | import android.util.SparseArray; 9 | 10 | public class PointList { 11 | 12 | public static final String OK="OK"; 13 | private SparseArray mArr = new SparseArray(); 14 | private int mNext; 15 | private int mMax; 16 | private int mFirst; 17 | private boolean mDirty=false; 18 | private int ii=0; 19 | private StorageHelper mStorageHelper; 20 | private List mRemoved=new ArrayList(); 21 | private Point mProximityOrigin; 22 | private boolean mUseMockRegistry=false; 23 | private boolean mShouldUseTrash; 24 | 25 | public PointList(int max) { 26 | fastClear(); 27 | mMax=max; 28 | } 29 | 30 | public PointList(int max, StorageHelper sh) { 31 | mStorageHelper=sh; 32 | fastClear(); 33 | mMax=max; 34 | } 35 | 36 | public void clear() { 37 | // puts removed points to trash 38 | int l=mArr.size(); 39 | int i=0; 40 | for (; i < l; i+=1) { 41 | mRemoved.add(mArr.valueAt(0)); 42 | mArr.removeAt(0); 43 | //onPointdeleted(); 44 | } 45 | mFirst=-1; 46 | mNext=1; 47 | mDirty=true; 48 | } 49 | 50 | public void fastClear() { 51 | // does not use trash 52 | mArr.clear(); 53 | mFirst=-1; 54 | mNext=1; 55 | mDirty=true; 56 | } 57 | 58 | public int adoptMax(int m) { 59 | if (m < 1) return mMax; 60 | // cannot be set to less than actual count 61 | if (m < mArr.size()) { mMax=mArr.size(); } //if m < mMax 62 | else { mMax=m; } 63 | return mMax; 64 | } 65 | 66 | public int getMax() { return mMax; } 67 | 68 | public int getSize() { return mArr.size(); } 69 | 70 | public String addAsNext(Point p) throws U.DataException { 71 | String s=""; 72 | if (mNext != p.getId()) throw new U.RunException("mNext="+mNext+", id="+p.getId()); 73 | if (mArr.size() >= mMax) { 74 | String r=trimFirst(); 75 | if (r != null) s="Removed "+r; 76 | } 77 | mArr.append(mNext, p); 78 | mDirty=true; 79 | mNext+=1; 80 | return s; 81 | } 82 | 83 | public String addAndShiftNext(Point p) throws U.DataException { 84 | String s=""; 85 | if (mArr.size() >= mMax) { 86 | String r=trimFirst(); 87 | if (r != null) s="Removed "+r; 88 | } 89 | int newId = Math.max(p.getId(), mNext); 90 | p.setId(newId); 91 | mArr.append(newId, p); 92 | mDirty=true; 93 | mNext=newId+1; 94 | return s; 95 | } 96 | 97 | private String trimFirst() throws U.DataException { 98 | Point toRemove=getEdge(); 99 | if (toRemove == null) return ""; 100 | mFirst=toRemove.getId(); 101 | Log.d(U.TAG,"PointList:"+"About to delete:"+String.valueOf(mFirst)); 102 | mRemoved.add(toRemove); 103 | mArr.delete(mFirst); 104 | //onPointdeleted(); 105 | String deletedId=String.valueOf(mFirst); 106 | mDirty=true; 107 | return deletedId; 108 | } 109 | 110 | public int getNext() { return mNext; } 111 | public String getNextS() { return String.valueOf(mNext); } 112 | 113 | public Point getEdge() throws U.DataException { 114 | if (U.DEBUG) Log.d(U.TAG,"PointList:"+"Array size "+mArr.size()+"/"+mMax); 115 | if (mArr.size() < mMax) { 116 | if (U.DEBUG) Log.d(U.TAG,"PointList:"+"No need to remove anything:"+mArr.size()+"/"+mMax); 117 | return null; 118 | } 119 | int i=0; 120 | Point p; 121 | while ( (p=mArr.valueAt(i)).isProtected() ) { 122 | i+=1; 123 | if (i >= mArr.size()) { 124 | if (U.DEBUG) Log.d(U.TAG,"PointList:"+"No removable points"); 125 | throw new U.DataException ("No room"); 126 | } 127 | } 128 | if (U.DEBUG) Log.d(U.TAG,"PointList:"+"Found edge point:"+String.valueOf(p.getId())); 129 | return p; 130 | } 131 | 132 | public boolean isEmpty() { return mArr.size() == 0; } 133 | 134 | public boolean isDirty() { return mDirty; } 135 | public void clearDirty() { mDirty=false; } 136 | public void setDirty() { mDirty=true; } 137 | 138 | public U.Summary load() throws IOException,U.DataException { 139 | U.Summary loaded=mStorageHelper.readPoints(this); 140 | mDirty=false; 141 | return loaded; 142 | } 143 | 144 | public U.Summary clearAndLoad() throws IOException,U.DataException { 145 | fastClear(); 146 | U.Summary loaded=mStorageHelper.readPoints(this); 147 | mDirty=true; 148 | return loaded; 149 | } 150 | 151 | public String save() { 152 | try { 153 | mStorageHelper.savePoints(this); 154 | if ( ! mRemoved.isEmpty()) tryUseTrash(); 155 | mDirty=false; 156 | return PointList.OK; 157 | } 158 | catch (IOException e) { 159 | Log.e(U.TAG,"PointList_save:"+e.getMessage()); 160 | return "File error:"+e.getMessage(); 161 | } 162 | catch (Exception e) { 163 | Log.e(U.TAG, "PointList_save:"+e.getMessage()); 164 | return "Save error:"+e.getMessage(); 165 | } 166 | } 167 | 168 | public void forceUseTrash() {// for tests 169 | mUseMockRegistry=true; 170 | mShouldUseTrash=true; 171 | } 172 | 173 | public void forceNotUseTrash() {// for tests 174 | mUseMockRegistry=true; 175 | mShouldUseTrash=false; 176 | } 177 | 178 | private boolean shouldUseTrash() { 179 | if (mUseMockRegistry) return mShouldUseTrash; 180 | return MyRegistry.getInstance().getBool("useTrash"); 181 | } 182 | 183 | private void tryUseTrash() throws IOException { 184 | if (shouldUseTrash()) { 185 | for ( Point p : mRemoved ) { mStorageHelper.trashPoint(p); }// normally it is only one point 186 | } 187 | mRemoved=new ArrayList(); 188 | } 189 | 190 | public String makeJsonPresentation() { 191 | JSONArray ja=new JSONArray(); 192 | int size=mArr.size(); 193 | if (size == 0) return ja.toString(); 194 | int i=0; 195 | for( ; i < size; i+=1) { 196 | Point p=mArr.valueAt(i); 197 | if (p.hasCoords()) { ja.put(p.makeJsonPresentation(mArr.keyAt(i))); } 198 | } 199 | return ja.toString(); 200 | } 201 | 202 | public int getIdByIndex(int i) { 203 | return mArr.keyAt(i); 204 | } 205 | 206 | public List getIndices() { 207 | List r = new ArrayList(); 208 | Point p; 209 | int id; 210 | while ((p=iterate()) != null) { 211 | id=p.getId(); 212 | r.add(String.valueOf(id)); 213 | } 214 | return r; 215 | } 216 | 217 | public Point iterate() { 218 | int size=mArr.size(); 219 | if (size == 0 || ii >= size) { 220 | ii=0; 221 | return null; 222 | } 223 | Point p=mArr.valueAt(ii); 224 | if (p.getId() <= 0) p.setId(mArr.keyAt(ii)); 225 | ii+=1; 226 | return p; 227 | } 228 | 229 | public Point getById(int id) { 230 | return mArr.get(id); 231 | } 232 | 233 | public void update(Point p) { 234 | int id=p.getId(); 235 | if (mArr.indexOfKey(id) < 0) { 236 | Log.e(U.TAG,"PointList:"+"Unknown id="+id); 237 | return; 238 | } 239 | mArr.put(id, p); 240 | mDirty=true; 241 | } 242 | 243 | public void moveUnprotectedToTrash(int id) { 244 | if (mArr.indexOfKey(id) < 0) { 245 | Log.e(U.TAG,"PointList:"+"Unknown id="+id); 246 | return; 247 | } 248 | Point p=mArr.get(id); 249 | if (p.isProtected()) return; 250 | mRemoved.add(p); 251 | mArr.delete(id); 252 | mDirty=true; 253 | } 254 | 255 | public int fastDeleteGroup(int from, int until) { 256 | int count=0, id; 257 | Point p; 258 | //Iterator ip=mArr.iterator(); does not work for SparseArray 259 | List toRemove= new ArrayList(); 260 | while ((p=iterate()) != null) { 261 | id=p.getId(); 262 | if (id >= from && (until < 0 || id <= until)) toRemove.add(id); 263 | } 264 | count=toRemove.size(); 265 | if (count > 0) { 266 | for (int i=0; i < count; i+=1) { mArr.delete(toRemove.get(i)); } 267 | mDirty=true; 268 | } 269 | return count; 270 | } 271 | 272 | public void renumber() { 273 | SparseArray a=new SparseArray(); 274 | int l=mArr.size(); 275 | Point p; 276 | for (int i=0; i < l; i+=1) { 277 | p=mArr.valueAt(i); 278 | p.setId(i+1); 279 | a.append(i+1,p); 280 | } 281 | mArr=a; 282 | mDirty=true; 283 | } 284 | 285 | public StorageHelper getStorageHelper() { return mStorageHelper; } 286 | 287 | public void setProximityOrigin(Point loc) { 288 | if (loc !=null && loc.hasCoords()) mProximityOrigin=(Point) loc.clone(); 289 | } 290 | 291 | public boolean hasProximityOrigin() { return mProximityOrigin != null; } 292 | public Point getProximityOrigin() { return mProximityOrigin; } 293 | 294 | public int findNearest(Point cursor) { 295 | double deg2rad = 0.0174532925199433; 296 | double cosLat=Math.cos(Double.parseDouble(cursor.lat)*deg2rad); 297 | double minSqDistance=1e99, sqDistance; 298 | int foundId=-1; 299 | Point p; 300 | 301 | while ((p=iterate()) != null) { 302 | if ( ! p.hasCoords()) continue; 303 | sqDistance=U.sqDistance(p, cursor, cosLat); 304 | if (sqDistance < minSqDistance) { 305 | minSqDistance=sqDistance; 306 | foundId=p.getId(); 307 | } 308 | } 309 | return foundId; 310 | } 311 | 312 | } 313 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/SingleFragmentActivity.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import android.os.Bundle; 4 | //import android.support.v4.app.Fragment; 5 | import androidx.fragment.app.Fragment; 6 | //import android.support.v4.app.FragmentManager; 7 | import androidx.fragment.app.FragmentManager; 8 | //import android.support.v7.app.AppCompatActivity; 9 | import androidx.appcompat.app.AppCompatActivity; 10 | 11 | public abstract class SingleFragmentActivity extends AppCompatActivity { 12 | //protected abstract android.support.v4.app.Fragment createFragment(); 13 | protected abstract androidx.fragment.app.Fragment createFragment(); 14 | 15 | @Override 16 | public void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_fragment); 19 | FragmentManager fm = getSupportFragmentManager(); 20 | Fragment fragment = fm.findFragmentById(R.id.fragment_container); 21 | 22 | if (fragment == null) { 23 | fragment = createFragment(); 24 | fm.beginTransaction() 25 | .add(R.id.fragment_container, fragment) 26 | .commit(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/StorageHelper.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.Map; 6 | 7 | import android.content.Context; 8 | import android.text.TextUtils; 9 | import android.util.Log; 10 | import truewatcher.tower.U.DataException; 11 | 12 | public class StorageHelper { 13 | private String mMyFile; 14 | private String mMyExt="csv"; 15 | private String mHeader=TextUtils.join(Point.SEP, Point.FIELDS); 16 | private String mPath; 17 | private String mMyTrashFile="trash.csv"; 18 | 19 | public static String getWorkingFolder(Context context, MyRegistry mRg) throws U.FileException { 20 | String nativeFolder = context.getExternalFilesDir(null).getPath(); 21 | String targetPath = nativeFolder; 22 | if (mRg.getBool("useMediaFolder")) { 23 | String appMediaFolder = getMediaDir(context); 24 | Log.i(U.TAG, "appMediaFolder=" + appMediaFolder); 25 | if (appMediaFolder.isEmpty()) throw new U.FileException("Cannot find the app's media folder"); 26 | targetPath = appMediaFolder; 27 | } 28 | return targetPath; 29 | } 30 | 31 | public static String getMediaDir(Context context) { 32 | // context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) gives a subfolder of Android/data 33 | //https://stackoverflow.com/questions/69658332/how-to-create-folder-inside-android-media-in-android-11 34 | File[] dirs = new File[0]; 35 | dirs = context.getExternalMediaDirs(); 36 | for (int i = 0; i= from && (until < 0 || id <= until)) { 79 | sb.append(p.toCsv()).append(Point.NL); 80 | count+=1; 81 | } 82 | } 83 | buf=sb.toString(); 84 | if (convertTo.equals("gpx")) { 85 | GpxHelper gh=new GpxHelper(); 86 | buf=gh.csv2gpx(buf); 87 | outcome=new U.Summary("exported", fullCount, gh.mCount, targetFile); 88 | } 89 | else outcome=new U.Summary("wrote", fullCount, count, targetFile); 90 | U.filePutContents(mPath, targetFile, buf, false); 91 | return outcome; 92 | } 93 | 94 | public void trashPoint(Point p) throws IOException { 95 | if (p == null) { 96 | Log.w(U.TAG,"StorageHelper:"+"trashPoint : null argument"); 97 | return; 98 | } 99 | String h=mHeader+Point.NL; 100 | String buf = p.toCsv()+Point.NL; 101 | if ( U.fileExists(mPath, mMyTrashFile, "csv") == null) buf=h+buf; 102 | U.filePutContents(mPath, mMyTrashFile, buf, true); 103 | } 104 | 105 | public int getPointCount(String targetFile) throws IOException,U.DataException { 106 | targetFile=U.assureExtension(targetFile,"csv"); 107 | String buf=U.fileGetContents(mPath, targetFile); 108 | String[] lines=splitCsv(buf); 109 | int l=lines.length; 110 | int expected=l-2; 111 | if (l == 1) expected=0; 112 | return expected; 113 | } 114 | 115 | public int checkPointCount(String targetFile, PointList pl) throws IOException,U.DataException { 116 | int expected=getPointCount(targetFile); 117 | checkWithListSize(expected,pl); 118 | return expected; 119 | } 120 | 121 | private String[] splitCsv(String buf) throws DataException { 122 | String[] lines=TextUtils.split(buf, Point.NL); 123 | int l=lines.length; 124 | if (l == 0) throw new U.DataException("The file has no header line"); 125 | if ( ! lines[0].equals(mHeader)) throw new U.DataException("The file has wrong header line"); 126 | return lines; 127 | } 128 | 129 | private void checkWithListSize(int expected, PointList pl) throws DataException { 130 | int maxCount=pl.getMax(); 131 | if (expected > maxCount) { throw new U.DataException("No room!" 132 | +" Set max point count to at least "+expected); } 133 | } 134 | 135 | public U.Summary readPoints(PointList pl) throws IOException,U.DataException { 136 | U.Summary s=readPoints(pl, mMyFile, 0, ""); 137 | pl.clearDirty(); 138 | return s; 139 | } 140 | 141 | public U.Summary readPoints(PointList pl, String targetFile, int currentPointCount) throws IOException,U.DataException { 142 | return readPoints(pl, targetFile, currentPointCount, ""); 143 | } 144 | 145 | public U.Summary readPoints(PointList pl, String targetFile, int currentPointCount, String convertFrom) 146 | throws IOException,U.DataException { 147 | if (null == U.fileExists(mPath, targetFile)) { 148 | return new U.Summary("loaded", 0, 0, targetFile); 149 | } 150 | String buf=U.fileGetContents(mPath, targetFile); 151 | if (convertFrom.equals("gpx")) { 152 | //buf=GpxHelper.removeGpxTail(buf); 153 | GpxHelper gh=new GpxHelper(); 154 | buf=gh.gpx2csv(buf); 155 | } 156 | 157 | String[] lines=splitCsv(buf); 158 | int l=lines.length; 159 | if (l == 1) return new U.Summary("loaded", 0, 0, mMyFile); 160 | int expected=currentPointCount+l-2;// 2 for the header and ending NL 161 | checkWithListSize(expected,pl); 162 | 163 | String line; 164 | Point p; 165 | int i=1; 166 | int count=0; 167 | int maxCount=pl.getMax(); 168 | for (; i < l; i+=1) { 169 | line=lines[i].trim(); 170 | if (line.isEmpty()) continue; 171 | p=(new Point()).fromCsv(line); 172 | if (U.DEBUG) Log.d(U.TAG,"StorageHelper:"+"About to add point "+p.getId()); 173 | if (p.getId() == pl.getNext()) { pl.addAsNext(p); } 174 | else { pl.addAndShiftNext(p); } 175 | count+=1; 176 | if (count >= maxCount) { 177 | if (U.DEBUG) Log.d(U.TAG,"StorageHelper:"+"Loaded first "+count+" points of "+(l-2)); 178 | break; 179 | } 180 | } 181 | if (count > 0) pl.setDirty(); 182 | return new U.Summary("loaded", l-2, count, targetFile); 183 | } 184 | 185 | public static String append2LatLonString(String unit, boolean isNewSeg, String lls) { 186 | StringBuilder buf; 187 | if (null == lls || lls.length() < 4) { 188 | return "[["+unit+"]]"; 189 | } 190 | if (isNewSeg) unit="],["+unit; 191 | else unit=","+unit; 192 | String cutEnding=lls.substring(0, lls.length()-2);// minus "]]" 193 | buf=new StringBuilder(cutEnding).append(unit).append("]]"); 194 | return buf.toString(); 195 | } 196 | 197 | public String getMyDir() { return mPath; } 198 | } 199 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/TestHelper.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import java.util.List; 4 | 5 | import android.text.TextUtils; 6 | import android.widget.TextView; 7 | 8 | public class TestHelper { 9 | private static TestHelper sMe; 10 | private static TextView sOutputElement; 11 | private static String sOutput=""; 12 | private int mCountAssert=0; 13 | 14 | public static TestHelper getInstance(TextView aView) { 15 | if(sMe == null) { 16 | sMe=new TestHelper(); 17 | } 18 | sOutputElement=aView; 19 | return sMe; 20 | } 21 | 22 | public void print(String s) { 23 | sOutput=sOutput.concat(s); 24 | sOutputElement.setText(sOutput); 25 | } 26 | 27 | public void println(String s) { 28 | //sOutput=sOutput.concat("\n"); 29 | print(s.concat("\n")); 30 | } 31 | 32 | public void printlnln(String s) { 33 | sOutput=sOutput.concat("\n"); 34 | print(s.concat("\n")); 35 | } 36 | 37 | public static class TestFailure extends Exception { 38 | 39 | public TestFailure(String aMessage) { super(aMessage); } 40 | } 41 | 42 | public void assertTrue(boolean aStatement, String aMessage, String aMessageOk, String aExplanation) throws TestFailure { 43 | incCount(); 44 | String out=""; 45 | if (aStatement) { 46 | out="Passed "+mCountAssert; 47 | if ( aMessageOk != null && ! aMessageOk.isEmpty() ) out+=": "+aMessageOk; 48 | println(out); 49 | return; 50 | } 51 | out="Failed "+mCountAssert; 52 | if ( aExplanation != null && ! aExplanation.isEmpty() ) out+=": "+aExplanation; 53 | //out+=": "+aMessage; 54 | println(out); 55 | throw new TestFailure(aMessage); 56 | } 57 | 58 | public void assertTrue(boolean aStatement, String aMessage, String aMessageOk) throws TestFailure { 59 | assertTrue(aStatement, aMessage, aMessageOk, ""); 60 | } 61 | 62 | public void assertTrue(boolean aStatement, String aMessage) throws TestFailure { 63 | assertTrue(aStatement, aMessage, "", ""); 64 | } 65 | 66 | public void assertEquals(int aExpected,int aFound, String aMessage, String aMessageOk) throws TestFailure { 67 | String expl=aFound+" does not equal to the expected "+aExpected; 68 | assertTrue(aExpected == aFound, aMessage, aMessageOk, expl); 69 | } 70 | 71 | public void assertEquals(String aExpected,String aFound, String aMessage, String aMessageOk) throws TestFailure { 72 | String expl=aFound+" does not equal to the expected "+aExpected; 73 | assertTrue(aExpected.equals(aFound), aMessage, aMessageOk, expl); 74 | } 75 | 76 | public void assertEqualsList(List aExpected, List aFound, String aMessage, String aMessageOk) throws TestFailure { 77 | String e=TextUtils.join(", ", aExpected); 78 | String f=TextUtils.join(", ", aFound); 79 | String expl=f+" does not equal to the expected "+e; 80 | assertTrue(e.equals(f), aMessage, aMessageOk, expl); 81 | } 82 | 83 | public void assertContains(String aExpected,String aHaystack, String aMessage, String aMessageOk) throws TestFailure { 84 | String expl=aHaystack+" does not contain "+aExpected; 85 | assertTrue(aHaystack.indexOf(aExpected) >= 0, aMessage, aMessageOk, expl); 86 | } 87 | 88 | public void assertNotContains(String aExpected,String aHaystack, String aMessage, String aMessageOk) throws TestFailure { 89 | String expl=aHaystack+" still contains "+aExpected; 90 | assertTrue(aHaystack.indexOf(aExpected) < 0, aMessage, aMessageOk, expl); 91 | } 92 | 93 | public void csvLineDiff(String aExpected, String aObtained, String SEP) throws TestFailure { 94 | aExpected=aExpected.trim(); 95 | String[] esa=TextUtils.split(aExpected, SEP); 96 | aObtained=aObtained.trim(); 97 | String[] osa=TextUtils.split(aObtained, SEP); 98 | if (esa.length != osa.length) throw new TestFailure("Expected "+esa.length+" fields, got "+osa.length); 99 | String SAME="="; 100 | String DIFF="/"; 101 | String ef,of; 102 | String[] rsa=new String[esa.length]; 103 | for (int i=0; i < esa.length; i+=1) { 104 | ef=esa[i]; 105 | of=osa[i]; 106 | if (ef.isEmpty() && of.isEmpty()) { rsa[i]=""; } 107 | else if (ef.equals(of)) { rsa[i]=SAME; } 108 | else { rsa[i]=ef+DIFF+of; } 109 | } 110 | println(TextUtils.join(SEP,rsa)); 111 | } 112 | 113 | private void incCount() { mCountAssert+=1; } 114 | } 115 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/TrackListener.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import android.content.Context; 4 | import android.location.Location; 5 | import android.location.LocationListener; 6 | import android.location.LocationManager; 7 | import android.os.Bundle; 8 | import android.util.Log; 9 | 10 | public class TrackListener implements LocationListener { 11 | 12 | private boolean mOn=false; 13 | private int mCounter=0; 14 | public String status=" new "; 15 | public long prevUpdateTime=0; 16 | public long updateTime=0; 17 | public long startUpdatesTime=0; 18 | private LocationManager mLocationManager=null;// same instance for startListening and stopListening ! 19 | //private Model.LocationReceiver mLocationReceiver = new Model.LocationReceiver(); 20 | private MyRegistry mRegistry = MyRegistry.getInstance(); 21 | private TrackStorage mTrackStorage=null;//=Model.getInstance().getTrackStorage(); causes loop 22 | private TrackPointListener mListener=null; 23 | 24 | public TrackListener(TrackStorage aTrackStorage) { 25 | mTrackStorage=aTrackStorage; 26 | } 27 | 28 | public boolean isOn() { return mOn; } 29 | public void setOn() { mOn=true; } 30 | public void setOff() { mOn=false; } 31 | 32 | public void clearCounter() { mCounter=0; } 33 | public void incCounter() { mCounter+=1; } 34 | public int getCounter() { return mCounter; } 35 | 36 | public void startListening(Context ct) { 37 | long minTimeMs=1000* mRegistry.getInt("gpsMinDelayS");//U.minFixDelayS; 38 | float minDistanceM= mRegistry.getInt("gpsMinDistance");//0; 39 | try { 40 | mLocationManager = (LocationManager) ct.getSystemService(Context.LOCATION_SERVICE); 41 | mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, minTimeMs, minDistanceM, 42 | this); 43 | prevUpdateTime=updateTime=startUpdatesTime=U.getTimeStamp(); 44 | //if (U.DEBUG) Log.d(U.TAG,"TrackListener:"+"started"); 45 | } 46 | catch (SecurityException e) { 47 | Log.i(U.TAG, "TrackListener:"+"Security Exception"+e.getMessage()); 48 | throw new U.RunException(e.getMessage()); 49 | } 50 | } 51 | 52 | public void stopListening() { 53 | try { 54 | if (null == mLocationManager) return; 55 | mLocationManager.removeUpdates(this); 56 | mLocationManager = null; 57 | } 58 | catch (SecurityException e) { 59 | throw new U.RunException(e.getMessage()); 60 | } 61 | } 62 | 63 | public void onLocationChanged(Location loc) { 64 | if (U.DEBUG) Log.i(U.TAG, "LocationReceiver:"+"got a location " + loc.toString()); 65 | incCounter(); 66 | prevUpdateTime=updateTime; 67 | updateTime = U.getTimeStamp(); 68 | if (prevUpdateTime > 0) { 69 | long delay = updateTime - prevUpdateTime; 70 | if (delay - mRegistry.getInt("gpsMinDelayS") > mRegistry.getInt("gpsTimeoutS")) { 71 | mTrackStorage.saveNote("delay=" + Long.toString(delay) + "s", ""); 72 | } 73 | } 74 | onPointavailable(loc); 75 | } 76 | 77 | public void onStatusChanged(String provider, int status, Bundle extras) { 78 | if ( ! provider.equals(LocationManager.GPS_PROVIDER)) return; 79 | if (U.DEBUG) Log.i(U.TAG, "got new GPS status:"+String.valueOf(status)); 80 | } 81 | 82 | @Override 83 | public void onProviderEnabled(String provider) {} 84 | 85 | @Override 86 | public void onProviderDisabled(String provider) {} 87 | 88 | private void onPointavailable(Location loc) { 89 | Trackpoint p=new Trackpoint(loc); 90 | p=mTrackStorage.simplySave(p);// may set newSegment 91 | Model.getInstance().getJSbridge().consumeTrackpoint(p); 92 | if (null != mListener) mListener.onTrackpointAvailable(p); 93 | } 94 | 95 | public static interface TrackPointListener { 96 | public void onTrackpointAvailable(Trackpoint p); 97 | } 98 | 99 | public void attachListener(TrackPointListener l) { mListener=l; } 100 | 101 | public void removeListener(TrackPointListener l) { 102 | mListener=null; 103 | //if (mListener == null) return; 104 | //if (mListener != l) throw new U.RunException("Unregistering of unknown listener"); 105 | //mListener=null; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/truewatcher/tower/Trackpoint.java: -------------------------------------------------------------------------------- 1 | package truewatcher.tower; 2 | 3 | import java.util.Set; 4 | import org.json.JSONArray; 5 | import android.location.Location; 6 | import android.text.TextUtils; 7 | import java.text.SimpleDateFormat; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Calendar; 11 | import java.util.Collections; 12 | import java.util.HashSet; 13 | import java.util.List; 14 | 15 | public class Trackpoint extends LatLon implements Cloneable { 16 | private int mId=0; 17 | private String mType="T"; 18 | private static final Set TYPES = new HashSet(Arrays.asList( 19 | new String[] {"T","note"} 20 | )); 21 | public String alt=""; 22 | public String range=""; 23 | public String time=getDate(); 24 | public String data=""; 25 | private String mNewSegment=""; 26 | public String comment=""; 27 | // https://www.gpsvisualizer.com/tutorials/tracks.html 28 | public static final List FIELDS = Collections.unmodifiableList(Arrays.asList( 29 | new String[] {"type","new_track","time","lat","lon","alt","range","name","data"} 30 | )); 31 | public static final String SEP = ";"; 32 | public static final String NL = "\n"; 33 | 34 | public Trackpoint() { } 35 | 36 | public Trackpoint(String aType, String aComment, String aData) { 37 | if ( ! aType.equals("note")) throw new U.RunException("Trackpoint note of wrong type="+aType); 38 | setType(aType); 39 | this.comment=aComment; 40 | this.data=aData; 41 | } 42 | 43 | public Trackpoint(Location loc) { 44 | this.lat=String.valueOf(loc.getLatitude()); 45 | this.lon=String.valueOf(loc.getLongitude()); 46 | if (loc.hasAltitude()) this.alt=String.valueOf(loc.getAltitude()); 47 | if (loc.hasAccuracy()) this.range=String.valueOf(loc.getAccuracy()); 48 | } 49 | 50 | public Object clone() { 51 | try { return super.clone(); } 52 | catch ( CloneNotSupportedException e ) { return null; } 53 | } 54 | 55 | public void setId(int i) { mId=i; } 56 | public int getId() { return mId; } 57 | 58 | public void setType(String t) { 59 | if ( ! TYPES.contains(t) ) throw new U.RunException("Unhnown type="+t); 60 | mType=t; 61 | } 62 | 63 | public String getType() { return mType; } 64 | 65 | public void setNewSegment() { mNewSegment="1"; } 66 | public void setNewSegment(String s) { mNewSegment=s; } 67 | public boolean isNewSegment() { return ! mNewSegment.isEmpty(); } 68 | 69 | public static String getDate() { 70 | SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 71 | String fd = df.format(Calendar.getInstance().getTime()); 72 | return fd; 73 | } 74 | 75 | public JSONArray makeJsonPresentation() { 76 | JSONArray ja=new JSONArray(); 77 | if ( ! mType.equals("T")) return ja; 78 | ja.put(lat); 79 | ja.put(lon); 80 | return ja;//.toString(); 81 | } 82 | 83 | public String toCsv() { 84 | List ls=new ArrayList(); 85 | ls.add(ne(mType)); 86 | ls.add(ne(mNewSegment)); 87 | ls.add(ne(time)); 88 | ls.add(ne(lat)); 89 | ls.add(ne(lon)); 90 | ls.add(ne(alt)); 91 | ls.add(ne(range)); 92 | ls.add(ne(comment)); 93 | ls.add(ne(data)); 94 | String s= TextUtils.join(Trackpoint.SEP, ls); 95 | return s; 96 | } 97 | 98 | public Trackpoint fromCsv(String s) throws U.DataException { 99 | s=s.trim(); 100 | if (s.isEmpty()) return null; 101 | String[] ls=TextUtils.split(s, Trackpoint.SEP); 102 | if (ls.length != Trackpoint.FIELDS.size()) { 103 | throw new U.DataException("Source has "+ls.length+" fields, while "+Trackpoint.FIELDS.size()+" are required"); 104 | } 105 | setType(ls[0]); 106 | if ( ! ls[1].isEmpty()) setNewSegment(ls[1]); 107 | time=ls[2]; 108 | lat=ls[3]; 109 | lon=ls[4]; 110 | alt=ls[5]; 111 | range=ls[6]; 112 | comment=ls[7]; 113 | data=ls[8]; 114 | return this; 115 | } 116 | 117 | private String ne(String s) { 118 | if (s == null || s.isEmpty()) return ""; 119 | if (s.contains(SEP)) throw new U.RunException("Misplaced separator"); 120 | if (s.contains(NL)) throw new U.RunException("Misplaced NL"); 121 | return s; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrueWatcher/tower/6699f23ed40de1991d5b2304d99e40747054d5e3/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrueWatcher/tower/6699f23ed40de1991d5b2304d99e40747054d5e3/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrueWatcher/tower/6699f23ed40de1991d5b2304d99e40747054d5e3/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrueWatcher/tower/6699f23ed40de1991d5b2304d99e40747054d5e3/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_build_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_chevron_left_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done_black_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder_open_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_format_list_bulleted_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher3_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher3_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 13 | 18 | 23 | 28 | 33 | 38 | 43 | 47 | 51 | 55 | 59 | 64 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_map_white_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 19 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_target_variant_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_fragment.xml: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_add_point.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 13 | 17 | 18 | 22 | 23 | 27 | 28 | 33 | 34 | 39 | 40 | 41 | 42 | 46 | 47 | 54 | 55 | 61 | 62 | 63 | 64 | 68 | 69 | 75 | 76 | 80 | 81 | 85 | 86 | 90 | 91 | 97 | 98 | 102 | 103 |