├── .gitignore ├── Half_Marathon.gpx ├── LICENSE ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── goldrushcomputing │ │ └── androidlocationstarterkitinkotlin │ │ └── ExampleInstrumentedTest.kt │ ├── debug │ └── AndroidManifest.xml │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── goldrushcomputing │ │ │ └── androidlocationstarterkitinkotlin │ │ │ ├── KalmanLatLong.kt │ │ │ ├── LocationService.kt │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xxhdpi │ │ ├── inaccurate_location_marker.png │ │ ├── kalman_ng_location_marker.png │ │ ├── no_accuracy_location_marker.png │ │ ├── old_location_marker.png │ │ ├── run_start_button.png │ │ ├── run_stop_button.png │ │ └── user_position_point.png │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── goldrushcomputing │ └── androidlocationstarterkitinkotlin │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Built application files 4 | *.apk 5 | *.ap_ 6 | *.aab 7 | 8 | 9 | # Files for the ART/Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | out/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # Intellij 40 | *.iml 41 | #.idea/workspace.xml 42 | #.idea/tasks.xml 43 | #.idea/gradle.xml 44 | #.idea/assetWizardSettings.xml 45 | #.idea/dictionaries 46 | #.idea/libraries 47 | #.idea/caches 48 | .idea/ 49 | 50 | # Keystore files 51 | .jks 52 | *.keystore 53 | 54 | # External native build folder generated in Android Studio 2.2 and later 55 | .externalNativeBuild 56 | 57 | # Google Services (e.g. APIs or Firebase) 58 | google-services.json 59 | 60 | # Freeline 61 | freeline.py 62 | freeline/ 63 | freeline_project_description.json 64 | 65 | 66 | # Goole Map API Key 67 | google_maps_api.xml -------------------------------------------------------------------------------- /Half_Marathon.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Takamitsu Mizutori 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | id 'com.google.secrets_gradle_plugin' version '0.5' 6 | } 7 | 8 | android { 9 | compileSdkVersion 31 10 | defaultConfig { 11 | applicationId "com.goldrushcomputing.androidlocationstarterkitinkotlin" 12 | minSdkVersion 30 13 | targetSdkVersion 31 14 | versionCode 2 15 | versionName "1.2" 16 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation 'androidx.core:core-ktx:1.7.0' 28 | implementation 'androidx.appcompat:appcompat:1.4.1' 29 | implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' 30 | implementation 'com.google.android.gms:play-services-maps:18.0.2' 31 | implementation 'com.google.android.material:material:1.6.0' 32 | 33 | testImplementation 'junit:junit:4.13.2' 34 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 35 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 36 | } 37 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/goldrushcomputing/androidlocationstarterkitinkotlin/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.goldrushcomputing.androidlocationstarterkitinkotlin 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.goldrushcomputing.androidlocationstarterkitinkotlin", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 25 | 33 | 36 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/goldrushcomputing/androidlocationstarterkitinkotlin/KalmanLatLong.kt: -------------------------------------------------------------------------------- 1 | package com.goldrushcomputing.androidlocationstarterkitinkotlin 2 | 3 | /** 4 | * Created by Takamitsu Mizutori on 2018/12/08. 5 | */ 6 | class KalmanLatLong(Q_metres_per_second: Float) { 7 | private val MinAccuracy = 1f 8 | 9 | private var Q_metres_per_second: Float = 0.toFloat() 10 | private var TimeStamp_milliseconds: Long = 0 11 | private var lat: Double = 0.toDouble() 12 | private var lng: Double = 0.toDouble() 13 | private var variance: Float = -1f // P matrix. Negative means object uninitialised. 14 | // NB: units irrelevant, as long as same units used 15 | // throughout 16 | var consecutiveRejectCount: Int = 0 17 | 18 | init { 19 | this.Q_metres_per_second = Q_metres_per_second 20 | } 21 | 22 | /* 23 | fun KalmanLatLong(Q_metres_per_second: Float): ??? { 24 | //this.Q_metres_per_second = Q_metres_per_second 25 | variance = -1f 26 | consecutiveRejectCount = 0 27 | } 28 | */ 29 | 30 | fun get_TimeStamp(): Long { 31 | return TimeStamp_milliseconds 32 | } 33 | 34 | fun get_lat(): Double { 35 | return lat 36 | } 37 | 38 | fun get_lng(): Double { 39 | return lng 40 | } 41 | 42 | fun get_accuracy(): Float { 43 | return Math.sqrt(variance.toDouble()).toFloat() 44 | } 45 | 46 | fun SetState( 47 | lat: Double, lng: Double, accuracy: Float, 48 | TimeStamp_milliseconds: Long 49 | ) { 50 | this.lat = lat 51 | this.lng = lng 52 | variance = accuracy * accuracy 53 | this.TimeStamp_milliseconds = TimeStamp_milliseconds 54 | } 55 | 56 | // / 57 | // / Kalman filter processing for lattitude and longitude 58 | // / 59 | // / new measurement of 60 | // lattidude 61 | // / new measurement of longitude 62 | // / measurement of 1 standard deviation error in 63 | // metres 64 | // / time of measurement 65 | // / new state 66 | fun Process( 67 | lat_measurement: Double, lng_measurement: Double, 68 | accuracy: Float, TimeStamp_milliseconds: Long, Q_metres_per_second: Float 69 | ) { 70 | var accuracy = accuracy 71 | this.Q_metres_per_second = Q_metres_per_second 72 | 73 | if (accuracy < MinAccuracy) 74 | accuracy = MinAccuracy 75 | if (variance < 0) { 76 | // if variance < 0, object is unitialised, so initialise with 77 | // current values 78 | this.TimeStamp_milliseconds = TimeStamp_milliseconds 79 | lat = lat_measurement 80 | lng = lng_measurement 81 | variance = accuracy * accuracy 82 | } else { 83 | // else apply Kalman filter methodology 84 | 85 | val TimeInc_milliseconds = TimeStamp_milliseconds - this.TimeStamp_milliseconds 86 | if (TimeInc_milliseconds > 0) { 87 | // time has moved on, so the uncertainty in the current position 88 | // increases 89 | variance += (TimeInc_milliseconds.toFloat() * Q_metres_per_second 90 | * Q_metres_per_second) / 1000 91 | this.TimeStamp_milliseconds = TimeStamp_milliseconds 92 | // TO DO: USE VELOCITY INFORMATION HERE TO GET A BETTER ESTIMATE 93 | // OF CURRENT POSITION 94 | } 95 | 96 | // Kalman gain matrix K = Covarariance * Inverse(Covariance + 97 | // MeasurementVariance) 98 | // NB: because K is dimensionless, it doesn't matter that variance 99 | // has different units to lat and lng 100 | val K = variance / (variance + accuracy * accuracy) 101 | // apply K 102 | lat += K * (lat_measurement - lat) 103 | lng += K * (lng_measurement - lng) 104 | // new Covarariance matrix is (IdentityMatrix - K) * Covarariance 105 | variance = (1 - K) * variance 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /app/src/main/java/com/goldrushcomputing/androidlocationstarterkitinkotlin/LocationService.kt: -------------------------------------------------------------------------------- 1 | package com.goldrushcomputing.androidlocationstarterkitinkotlin 2 | 3 | import android.app.Notification 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.app.Service 7 | import android.content.BroadcastReceiver 8 | import android.content.Context 9 | import android.content.Intent 10 | import android.content.IntentFilter 11 | import android.graphics.Color 12 | import android.location.Criteria 13 | import android.location.Location 14 | import android.location.LocationListener 15 | import android.location.LocationManager 16 | import android.os.BatteryManager 17 | import android.os.Binder 18 | import android.os.IBinder 19 | import android.os.SystemClock 20 | import android.util.Log 21 | import androidx.core.app.NotificationCompat 22 | import androidx.localbroadcastmanager.content.LocalBroadcastManager 23 | import java.io.FileWriter 24 | import java.io.IOException 25 | import java.text.SimpleDateFormat 26 | import java.util.* 27 | 28 | /** 29 | * Created by Takamitsu Mizutori on 2018/12/08. 30 | */ 31 | class LocationService: Service(), LocationListener { 32 | 33 | companion object { 34 | private val FOREGROUND_SERVICE_NOTIFICATION_ID = 12345 35 | private val FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_ID = "location_service_notification_channel" 36 | } 37 | 38 | private val LOG_TAG = LocationService::class.java.simpleName 39 | 40 | private val binder = LocationServiceBinder() 41 | private var isLocationManagerUpdatingLocation: Boolean = false 42 | 43 | var locationList: ArrayList 44 | 45 | var oldLocationList: ArrayList 46 | var noAccuracyLocationList: ArrayList 47 | var inaccurateLocationList: ArrayList 48 | var kalmanNGLocationList: ArrayList 49 | 50 | var isLogging: Boolean = false 51 | 52 | private var currentSpeed = 0.0f // meters/second 53 | 54 | private var kalmanFilter: KalmanLatLong 55 | private var runStartTimeInMillis: Long = 0 56 | 57 | var batteryLevelArray = ArrayList() 58 | var batteryLevelScaledArray = ArrayList() 59 | var batteryScale: Int = 0 60 | private var gpsCount: Int = 0 61 | 62 | 63 | /* Battery Consumption */ 64 | private var batteryInfoReceiver: BroadcastReceiver? = null 65 | 66 | init { 67 | isLocationManagerUpdatingLocation = false 68 | locationList = ArrayList() 69 | noAccuracyLocationList = ArrayList() 70 | oldLocationList = ArrayList() 71 | inaccurateLocationList = ArrayList() 72 | kalmanNGLocationList = ArrayList() 73 | kalmanFilter = KalmanLatLong(3f) 74 | 75 | isLogging = false 76 | 77 | batteryInfoReceiver = object : BroadcastReceiver() { 78 | override fun onReceive(ctxt: Context, intent: Intent) { 79 | val batteryLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) 80 | val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) 81 | 82 | val batteryLevelScaled = batteryLevel / scale.toFloat() 83 | 84 | batteryLevelArray.add(Integer.valueOf(batteryLevel)) 85 | batteryLevelScaledArray.add(java.lang.Float.valueOf(batteryLevelScaled)) 86 | batteryScale = scale 87 | } 88 | }.also { 89 | @Suppress("DEPRECATION") 90 | LocalBroadcastManager.getInstance(this@LocationService).registerReceiver(it, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) 91 | } 92 | } 93 | 94 | 95 | override fun onStartCommand(i: Intent, flags: Int, startId: Int): Int { 96 | super.onStartCommand(i, flags, startId) 97 | startForeground() 98 | return START_STICKY 99 | } 100 | 101 | override fun onBind(intent: Intent?): IBinder { 102 | return binder 103 | } 104 | 105 | override fun onRebind(intent: Intent) { 106 | Log.d(LOG_TAG, "onRebind ") 107 | } 108 | 109 | override fun onUnbind(intent: Intent): Boolean { 110 | Log.d(LOG_TAG, "onUnbind ") 111 | return true 112 | } 113 | 114 | override fun onDestroy() { 115 | Log.d(LOG_TAG, "onDestroy ") 116 | try { 117 | batteryInfoReceiver?.let{ 118 | unregisterReceiver(it) 119 | } 120 | } catch (ex: IllegalArgumentException) { 121 | ex.printStackTrace() 122 | } 123 | } 124 | 125 | //This is where we detect the app is being killed, thus stop service. 126 | override fun onTaskRemoved(rootIntent: Intent) { 127 | Log.d(LOG_TAG, "onTaskRemoved ") 128 | this.stopUpdatingLocation() 129 | stopForeground(true) 130 | } 131 | 132 | /** 133 | * Binder class 134 | * 135 | * @author Takamitsu Mizutori 136 | */ 137 | inner class LocationServiceBinder : Binder() { 138 | val service: LocationService 139 | get() = this@LocationService 140 | } 141 | 142 | override fun onLocationChanged(newLocation: Location) { 143 | Log.d(LOG_TAG, "(" + newLocation.latitude + "," + newLocation.longitude + ")") 144 | 145 | gpsCount++ 146 | 147 | if (isLogging) { 148 | //locationList.add(newLocation); 149 | filterAndAddLocation(newLocation) 150 | } 151 | 152 | val intent = Intent("LocationUpdated") 153 | intent.putExtra("location", newLocation) 154 | 155 | @Suppress("DEPRECATION") 156 | LocalBroadcastManager.getInstance(this.application).sendBroadcast(intent) 157 | } 158 | 159 | override fun onProviderEnabled(provider: String) { 160 | if (provider == LocationManager.GPS_PROVIDER) { 161 | notifyLocationProviderStatusUpdated(true) 162 | } 163 | } 164 | 165 | override fun onProviderDisabled(provider: String) { 166 | if (provider == LocationManager.GPS_PROVIDER) { 167 | notifyLocationProviderStatusUpdated(false) 168 | } 169 | } 170 | 171 | private fun notifyLocationProviderStatusUpdated(isLocationProviderAvailable: Boolean) { 172 | //Broadcast location provider status change here 173 | } 174 | 175 | fun startLogging() { 176 | isLogging = true 177 | } 178 | 179 | fun stopLogging() { 180 | if (locationList.size > 1 && batteryLevelArray.size > 1) { 181 | val currentTimeInMillis = SystemClock.elapsedRealtimeNanos() / 1000000 182 | val elapsedTimeInSeconds = (currentTimeInMillis - runStartTimeInMillis) / 1000 183 | var totalDistanceInMeters = 0f 184 | for (i in 0 until locationList.size - 1) { 185 | totalDistanceInMeters += locationList[i].distanceTo(locationList[i + 1]) 186 | } 187 | val batteryLevelStart = batteryLevelArray[0] 188 | val batteryLevelEnd = batteryLevelArray[batteryLevelArray.size - 1] 189 | 190 | val batteryLevelScaledStart = batteryLevelScaledArray[0] 191 | val batteryLevelScaledEnd = batteryLevelScaledArray[batteryLevelScaledArray.size - 1] 192 | 193 | saveLog( 194 | elapsedTimeInSeconds, 195 | totalDistanceInMeters.toDouble(), 196 | gpsCount, 197 | batteryLevelStart, 198 | batteryLevelEnd, 199 | batteryLevelScaledStart, 200 | batteryLevelScaledEnd 201 | ) 202 | } 203 | isLogging = false 204 | } 205 | 206 | 207 | fun startUpdatingLocation() { 208 | if (!this.isLocationManagerUpdatingLocation) { 209 | isLocationManagerUpdatingLocation = true 210 | runStartTimeInMillis = SystemClock.elapsedRealtimeNanos() / 1000000 211 | 212 | locationList.clear() 213 | oldLocationList.clear() 214 | noAccuracyLocationList.clear() 215 | inaccurateLocationList.clear() 216 | kalmanNGLocationList.clear() 217 | 218 | val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager 219 | 220 | //Exception thrown when GPS or Network provider were not available on the user's device. 221 | try { 222 | val criteria = Criteria() 223 | criteria.accuracy = 224 | Criteria.ACCURACY_FINE //setAccuracyは内部では、https://stackoverflow.com/a/17874592/1709287の用にHorizontalAccuracyの設定に変換されている。 225 | criteria.powerRequirement = Criteria.POWER_HIGH 226 | criteria.isAltitudeRequired = false 227 | criteria.isSpeedRequired = true 228 | criteria.isCostAllowed = true 229 | criteria.isBearingRequired = false 230 | 231 | //API level 9 and up 232 | criteria.horizontalAccuracy = Criteria.ACCURACY_HIGH 233 | criteria.verticalAccuracy = Criteria.ACCURACY_HIGH 234 | //criteria.setBearingAccuracy(Criteria.ACCURACY_HIGH); 235 | //criteria.setSpeedAccuracy(Criteria.ACCURACY_HIGH); 236 | 237 | val gpsFreqInMillis = 5000 238 | val gpsFreqInDistance = 5 // in meters 239 | 240 | @Suppress("DEPRECATION") 241 | locationManager.requestLocationUpdates( 242 | gpsFreqInMillis.toLong(), 243 | gpsFreqInDistance.toFloat(), 244 | criteria, 245 | this, 246 | null 247 | ) 248 | 249 | /* Battery Consumption Measurement */ 250 | gpsCount = 0 251 | batteryLevelArray.clear() 252 | batteryLevelScaledArray.clear() 253 | 254 | } catch (e: IllegalArgumentException) { 255 | e.localizedMessage?.let { Log.e(LOG_TAG, it) } 256 | } catch (e: SecurityException) { 257 | e.localizedMessage?.let { Log.e(LOG_TAG, it) } 258 | } catch (e: RuntimeException) { 259 | e.localizedMessage?.let { Log.e(LOG_TAG, it) } 260 | } 261 | } 262 | } 263 | 264 | 265 | fun stopUpdatingLocation() { 266 | if (this.isLocationManagerUpdatingLocation == true) { 267 | val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager 268 | locationManager.removeUpdates(this) 269 | isLocationManagerUpdatingLocation = false 270 | } 271 | } 272 | 273 | private fun getLocationAge(newLocation: Location): Long { 274 | val locationAge: Long 275 | val currentTimeInMilli = SystemClock.elapsedRealtimeNanos() / 1000000 276 | val locationTimeInMilli = newLocation.elapsedRealtimeNanos / 1000000 277 | locationAge = currentTimeInMilli - locationTimeInMilli 278 | return locationAge 279 | } 280 | 281 | 282 | private fun filterAndAddLocation(location: Location): Boolean { 283 | 284 | val age = getLocationAge(location) 285 | 286 | if (age > 5 * 1000) { //more than 5 seconds 287 | Log.d(LOG_TAG, "Location is old") 288 | oldLocationList.add(location) 289 | return false 290 | } 291 | 292 | if (location.accuracy <= 0) { 293 | Log.d(LOG_TAG, "Latitidue and longitude values are invalid.") 294 | noAccuracyLocationList.add(location) 295 | return false 296 | } 297 | 298 | //setAccuracy(newLocation.getAccuracy()); 299 | val horizontalAccuracy = location.accuracy 300 | if (horizontalAccuracy > 1000) { //10meter filter 301 | Log.d(LOG_TAG, "Accuracy is too low.") 302 | inaccurateLocationList.add(location) 303 | return false 304 | } 305 | 306 | /* Kalman Filter */ 307 | var Qvalue: Float = 3.0f 308 | 309 | val locationTimeInMillis = location.elapsedRealtimeNanos / 1000000 310 | val elapsedTimeInMillis = locationTimeInMillis - runStartTimeInMillis 311 | 312 | @Suppress("DEPRECATION") 313 | if (currentSpeed == 0.0f) { 314 | Qvalue = 3.0f //3 meters per second 315 | } else { 316 | Qvalue = currentSpeed // meters per second 317 | } 318 | 319 | kalmanFilter.Process(location.latitude, location.longitude, location.accuracy, elapsedTimeInMillis, Qvalue) 320 | val predictedLat = kalmanFilter.get_lat() 321 | val predictedLng = kalmanFilter.get_lng() 322 | 323 | val predictedLocation = Location("")//provider name is unecessary 324 | predictedLocation.latitude = predictedLat//your coords of course 325 | predictedLocation.longitude = predictedLng 326 | val predictedDeltaInMeters = predictedLocation.distanceTo(location) 327 | 328 | if (predictedDeltaInMeters > 60) { 329 | Log.d(LOG_TAG, "Kalman Filter detects mal GPS, we should probably remove this from track") 330 | kalmanFilter.consecutiveRejectCount += 1 331 | 332 | if (kalmanFilter.consecutiveRejectCount > 3) { 333 | kalmanFilter = KalmanLatLong(3f) //reset Kalman Filter if it rejects more than 3 times in raw. 334 | } 335 | 336 | kalmanNGLocationList.add(location) 337 | return false 338 | } else { 339 | kalmanFilter.consecutiveRejectCount = 0 340 | } 341 | 342 | /* Notifiy predicted location to UI */ 343 | val intent = Intent("PredictLocation") 344 | intent.putExtra("location", predictedLocation) 345 | @Suppress("DEPRECATION") 346 | LocalBroadcastManager.getInstance(this.application).sendBroadcast(intent) 347 | 348 | Log.d(LOG_TAG, "Location quality is good enough.") 349 | currentSpeed = location.speed 350 | locationList.add(location) 351 | 352 | return true 353 | } 354 | 355 | /* Data Logging */ 356 | @Synchronized 357 | fun saveLog( 358 | timeInSeconds: Long, 359 | distanceInMeters: Double, 360 | gpsCount: Int, 361 | batteryLevelStart: Int, 362 | batteryLevelEnd: Int, 363 | batteryLevelScaledStart: Float, 364 | batteryLevelScaledEnd: Float 365 | ) { 366 | val fileNameDateTimeFormat = SimpleDateFormat("yyyy_MMdd_HHmm", Locale.US) 367 | val filePath = (this.getExternalFilesDir(null)!!.absolutePath + "/" 368 | + fileNameDateTimeFormat.format(Date()) + "_battery" + ".csv") 369 | 370 | Log.d(LOG_TAG, "saving to $filePath") 371 | 372 | var fileWriter: FileWriter? = null 373 | try { 374 | fileWriter = FileWriter(filePath, false) 375 | fileWriter.append("Time,Distance,GPSCount,BatteryLevelStart,BatteryLevelEnd,BatteryLevelStart(/$batteryScale),BatteryLevelEnd(/$batteryScale)\n") 376 | val record = 377 | "$timeInSeconds,$distanceInMeters,$gpsCount,$batteryLevelStart,$batteryLevelEnd,$batteryLevelScaledStart,$batteryLevelScaledEnd\n" 378 | fileWriter.append(record) 379 | } catch (e: Exception) { 380 | e.printStackTrace() 381 | } finally { 382 | if (fileWriter != null) { 383 | try { 384 | fileWriter.close() 385 | } catch (ioe: IOException) { 386 | ioe.printStackTrace() 387 | } 388 | } 389 | } 390 | } 391 | 392 | private fun startForeground() { 393 | val notification = createNotification() 394 | startForeground(FOREGROUND_SERVICE_NOTIFICATION_ID, notification) 395 | } 396 | 397 | private fun createNotification(): Notification { 398 | /* Create Notification Channel */ 399 | val channelName = resources.getString(R.string.location_service_notification_channel_name) 400 | val channel = NotificationChannel(FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_ID, 401 | channelName, NotificationManager.IMPORTANCE_NONE) 402 | channel.description = getString(R.string.location_service_notification_channel_description) 403 | channel.lightColor = Color.BLUE 404 | channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC 405 | val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 406 | 407 | // After this, we can use FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_ID to refer the created channel 408 | service.createNotificationChannel(channel) 409 | 410 | /* Create Notification */ 411 | val notificationTitle = resources.getString(R.string.app_name) 412 | val notificationText = resources.getString(R.string.location_service_notification_text) 413 | 414 | // give FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_ID to use the channel we've just created. 415 | val notificationBuilder = NotificationCompat.Builder(this, FOREGROUND_SERVICE_NOTIFICATION_CHANNEL_ID ) 416 | 417 | return notificationBuilder 418 | .setOngoing(true) 419 | .setSmallIcon(R.mipmap.ic_launcher) 420 | .setContentTitle(notificationTitle) 421 | .setContentText(notificationText) 422 | .setCategory(Notification.CATEGORY_SERVICE) 423 | .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) 424 | .build() 425 | } 426 | } -------------------------------------------------------------------------------- /app/src/main/java/com/goldrushcomputing/androidlocationstarterkitinkotlin/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.goldrushcomputing.androidlocationstarterkitinkotlin 2 | 3 | import android.Manifest 4 | import android.content.* 5 | import android.content.pm.PackageManager 6 | import android.graphics.Color 7 | import android.location.Location 8 | import android.net.Uri 9 | import android.os.Bundle 10 | import android.os.Handler 11 | import android.os.IBinder 12 | import android.os.Looper 13 | import android.provider.Settings 14 | import android.util.Log 15 | import android.view.View 16 | import android.widget.ImageButton 17 | import androidx.activity.result.ActivityResultLauncher 18 | import androidx.activity.result.contract.ActivityResultContracts 19 | import androidx.appcompat.app.AppCompatActivity 20 | import androidx.core.app.ActivityCompat 21 | import androidx.localbroadcastmanager.content.LocalBroadcastManager 22 | import com.google.android.gms.maps.CameraUpdateFactory 23 | import com.google.android.gms.maps.GoogleMap 24 | import com.google.android.gms.maps.SupportMapFragment 25 | import com.google.android.gms.maps.model.* 26 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 27 | import java.util.* 28 | 29 | class MainActivity : AppCompatActivity() { 30 | private val TAG = "MainActivity" 31 | 32 | private lateinit var activityResultLauncher: ActivityResultLauncher 33 | private var map: GoogleMap? = null 34 | 35 | var locationService: LocationService? = null 36 | var isServiceBound = false 37 | 38 | private var userPositionMarker: Marker? = null 39 | private var locationAccuracyCircle: Circle? = null 40 | private var userPositionMarkerBitmapDescriptor: BitmapDescriptor? = null 41 | private var runningPathPolyline: Polyline? = null 42 | private val polylineOptions = PolylineOptions() 43 | private val polylineWidth = 30 44 | 45 | internal var zoomable = true 46 | 47 | internal var zoomBlockingTimer: Timer? = null 48 | private var didInitialZoom: Boolean = false 49 | 50 | private var locationUpdateReceiver: BroadcastReceiver? = null 51 | private var predictedLocationReceiver: BroadcastReceiver? = null 52 | 53 | private var startButton: ImageButton? = null 54 | private var stopButton: ImageButton? = null 55 | 56 | /* Filter */ 57 | private var predictionRange: Circle? = null 58 | private var oldLocationMarkerBitmapDescriptor: BitmapDescriptor? = null 59 | private var noAccuracyLocationMarkerBitmapDescriptor: BitmapDescriptor? = null 60 | private var inaccurateLocationMarkerBitmapDescriptor: BitmapDescriptor? = null 61 | private var kalmanNGLocationMarkerBitmapDescriptor: BitmapDescriptor? = null 62 | private var malMarkers = ArrayList() 63 | 64 | override fun onCreate(savedInstanceState: Bundle?) { 65 | super.onCreate(savedInstanceState) 66 | setContentView(R.layout.activity_main) 67 | 68 | activityResultLauncher = registerForActivityResult( 69 | ActivityResultContracts.RequestPermission() 70 | ) { isGranted -> 71 | if (isGranted && checkAccessFineLocationPermission()) { 72 | map?.let { 73 | onLocationPermissionGranted(it) 74 | } 75 | } else { 76 | showLocationPermissionDialog(isFirstTime = false) 77 | } 78 | } 79 | 80 | // Obtain the SupportMapFragment and get notified when the map is ready to be used. 81 | val mapFragment = supportFragmentManager 82 | .findFragmentById(R.id.map) as SupportMapFragment 83 | mapFragment.getMapAsync { googleMap -> 84 | /** 85 | * Manipulates the map once available. 86 | * This callback is triggered when the map is ready to be used. 87 | * This is where we can add markers or lines, add listeners or move the camera. In this case, 88 | * we just add a marker near Sydney, Australia. 89 | * If Google Play services is not installed on the device, the user will be prompted to install 90 | * it inside the SupportMapFragment. This method will only be triggered once the user has 91 | * installed Google Play services and returned to the app. 92 | */ 93 | googleMap.uiSettings.apply { 94 | isZoomControlsEnabled = false 95 | isCompassEnabled = true 96 | isMyLocationButtonEnabled = true 97 | } 98 | if (checkAccessFineLocationPermission()) { 99 | onLocationPermissionGranted(googleMap) 100 | } else { 101 | showLocationPermissionDialog(isFirstTime = true) 102 | } 103 | map = googleMap 104 | 105 | /* Start Location Service */ 106 | val locationService = Intent(this.application, LocationService::class.java) 107 | this.application.startForegroundService(locationService) 108 | isServiceBound = this.application.bindService(locationService, serviceConnection, Context.BIND_AUTO_CREATE) 109 | } 110 | 111 | 112 | locationUpdateReceiver = object : BroadcastReceiver() { 113 | override fun onReceive(context: Context, intent: Intent) { 114 | intent.getParcelableExtra("location")?.let{ newLocation -> 115 | drawLocationAccuracyCircle(newLocation) 116 | drawUserPositionMarker(newLocation) 117 | this@MainActivity.locationService?.let{ 118 | if (it.isLogging) { 119 | addPolyline() 120 | } 121 | } 122 | zoomMapTo(newLocation) 123 | /* Filter Visualization */ 124 | drawMalLocations() 125 | } 126 | } 127 | } 128 | 129 | predictedLocationReceiver = object : BroadcastReceiver() { 130 | override fun onReceive(context: Context, intent: Intent) { 131 | intent.getParcelableExtra("location")?.let{ predictedLocation -> 132 | drawPredictionRange(predictedLocation) 133 | } 134 | } 135 | } 136 | 137 | locationUpdateReceiver?.let{ 138 | @Suppress("DEPRECATION") 139 | LocalBroadcastManager.getInstance(this).registerReceiver( 140 | it, 141 | IntentFilter("LocationUpdated") 142 | ) 143 | } 144 | 145 | predictedLocationReceiver?.let{ 146 | @Suppress("DEPRECATION") 147 | LocalBroadcastManager.getInstance(this).registerReceiver( 148 | it, 149 | IntentFilter("PredictLocation") 150 | ) 151 | } 152 | 153 | startButton = this.findViewById(R.id.start_button) as ImageButton 154 | stopButton = this.findViewById(R.id.stop_button) as ImageButton 155 | stopButton?.visibility = View.INVISIBLE 156 | 157 | 158 | startButton?.setOnClickListener { 159 | startButton?.visibility = View.INVISIBLE 160 | stopButton?.visibility = View.VISIBLE 161 | 162 | clearPolyline() 163 | clearMalMarkers() 164 | this@MainActivity.locationService?.startLogging() 165 | } 166 | 167 | stopButton?.setOnClickListener { 168 | startButton?.visibility = View.VISIBLE 169 | stopButton?.visibility = View.INVISIBLE 170 | 171 | this@MainActivity.locationService?.stopLogging() 172 | } 173 | 174 | 175 | oldLocationMarkerBitmapDescriptor = BitmapDescriptorFactory.fromResource(R.drawable.old_location_marker) 176 | noAccuracyLocationMarkerBitmapDescriptor = 177 | BitmapDescriptorFactory.fromResource(R.drawable.no_accuracy_location_marker) 178 | inaccurateLocationMarkerBitmapDescriptor = 179 | BitmapDescriptorFactory.fromResource(R.drawable.inaccurate_location_marker) 180 | kalmanNGLocationMarkerBitmapDescriptor = 181 | BitmapDescriptorFactory.fromResource(R.drawable.kalman_ng_location_marker) 182 | 183 | 184 | } 185 | 186 | private fun checkAccessFineLocationPermission(): Boolean { 187 | return ActivityCompat.checkSelfPermission( 188 | this, 189 | Manifest.permission.ACCESS_FINE_LOCATION 190 | ) == PackageManager.PERMISSION_GRANTED 191 | } 192 | 193 | private fun showLocationPermissionDialog(isFirstTime: Boolean) { 194 | if(isFirstTime){ 195 | MaterialAlertDialogBuilder(this, R.style.DefaultAlertDialogStyle) 196 | .setTitle(R.string.map_dialog_ask_permission_title) 197 | .setMessage(R.string.map_dialog_ask_permission_description) 198 | .setPositiveButton(R.string.map_dialog_ask_permission_next) { _, _ -> 199 | activityResultLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) 200 | } 201 | .create() 202 | .show() 203 | }else{ 204 | //From the second time (coming back from the default location permission dialog) 205 | MaterialAlertDialogBuilder(this, R.style.DefaultAlertDialogStyle) 206 | .setTitle(R.string.map_dialog_no_permission_title) 207 | .setMessage(R.string.map_dialog_no_permission_description) 208 | .setPositiveButton(R.string.map_dialog_no_permission_do_not_allow) { _, _ -> } 209 | .setNegativeButton(R.string.map_dialog_no_permission_open_settings) { _, _ -> 210 | startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { 211 | data = Uri.fromParts("package", this@MainActivity.packageName, null) 212 | }) 213 | } 214 | .create() 215 | .show() 216 | } 217 | 218 | } 219 | 220 | private fun onLocationPermissionGranted(map: GoogleMap) { 221 | if (checkAccessFineLocationPermission()) { 222 | map.isMyLocationEnabled = false 223 | map.setOnCameraMoveStartedListener { reason -> 224 | if (reason == GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE) { 225 | Log.d(TAG, "onCameraMoveStarted after user's zoom action") 226 | zoomable = false 227 | zoomBlockingTimer?.cancel() 228 | val task = object : TimerTask() { 229 | override fun run() { 230 | Handler(Looper.getMainLooper()).post { // Update UI 231 | zoomBlockingTimer = null 232 | zoomable = true 233 | } 234 | } 235 | } 236 | zoomBlockingTimer = Timer() 237 | zoomBlockingTimer?.schedule(task, (10 * 1000).toLong()) 238 | Log.d(TAG, "start blocking auto zoom for 10 seconds") 239 | } 240 | } 241 | } 242 | } 243 | 244 | private val serviceConnection = object : ServiceConnection { 245 | override fun onServiceConnected(className: ComponentName, service: IBinder) { 246 | // This is called when the connection with the service has been 247 | // established, giving us the service object we can use to 248 | // interact with the service. Because we have bound to a explicit 249 | // service that we know is running in our own process, we can 250 | // cast its IBinder to a concrete class and directly access it. 251 | val name = className.className 252 | 253 | if (name.endsWith("LocationService")) { 254 | locationService = (service as LocationService.LocationServiceBinder).service 255 | 256 | this@MainActivity.locationService?.startUpdatingLocation() 257 | } 258 | } 259 | 260 | override fun onServiceDisconnected(className: ComponentName) { 261 | // This is called when the connection with the service has been 262 | // unexpectedly disconnected -- that is, its process crashed. 263 | // Because it is running in our same process, we should never 264 | // see this happen. 265 | if (className.className == "LocationService") { 266 | this@MainActivity.locationService?.stopUpdatingLocation() 267 | locationService = null 268 | } 269 | } 270 | } 271 | 272 | 273 | public override fun onDestroy() { 274 | try { 275 | if (locationUpdateReceiver != null) { 276 | unregisterReceiver(locationUpdateReceiver) 277 | } 278 | 279 | if (predictedLocationReceiver != null) { 280 | unregisterReceiver(predictedLocationReceiver) 281 | } 282 | } catch (ex: IllegalArgumentException) { 283 | ex.printStackTrace() 284 | } 285 | stopLocationService() 286 | super.onDestroy() 287 | } 288 | 289 | private fun stopLocationService(){ 290 | locationService?.let{ 291 | if(it.isLogging){ 292 | it.stopLogging() 293 | } 294 | it.stopUpdatingLocation() 295 | if(isServiceBound){ 296 | this.application.unbindService(serviceConnection) 297 | isServiceBound = false 298 | it.stopForeground(true) 299 | } 300 | } 301 | } 302 | 303 | private fun zoomMapTo(location: Location) { 304 | val latLng = LatLng(location.latitude, location.longitude) 305 | 306 | if (!this.didInitialZoom) { 307 | try { 308 | map?.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 17.5f)) 309 | this.didInitialZoom = true 310 | return 311 | } catch (e: Exception) { 312 | e.printStackTrace() 313 | } 314 | //Toast.makeText(this.getActivity(), "Inital zoom in process", Toast.LENGTH_LONG).show(); 315 | } 316 | 317 | if (zoomable) { 318 | try { 319 | zoomable = false 320 | map?.animateCamera(CameraUpdateFactory.newLatLng(latLng), 321 | object : GoogleMap.CancelableCallback { 322 | override fun onFinish() { 323 | zoomable = true 324 | } 325 | 326 | override fun onCancel() { 327 | zoomable = true 328 | } 329 | }) 330 | } catch (e: Exception) { 331 | e.printStackTrace() 332 | } 333 | } 334 | } 335 | 336 | 337 | private fun drawUserPositionMarker(location: Location) { 338 | val latLng = LatLng(location.latitude, location.longitude) 339 | 340 | if (this.userPositionMarkerBitmapDescriptor == null) { 341 | userPositionMarkerBitmapDescriptor = BitmapDescriptorFactory.fromResource(R.drawable.user_position_point) 342 | } 343 | 344 | userPositionMarker?.let{ 345 | it.position = latLng 346 | } ?: run{ 347 | userPositionMarker = map?.addMarker( 348 | MarkerOptions() 349 | .position(latLng) 350 | .flat(true) 351 | .anchor(0.5f, 0.5f) 352 | .icon(this.userPositionMarkerBitmapDescriptor) 353 | ) 354 | } 355 | } 356 | 357 | 358 | private fun drawLocationAccuracyCircle(location: Location) { 359 | if (location.accuracy < 0) { 360 | return 361 | } 362 | 363 | val latLng = LatLng(location.latitude, location.longitude) 364 | 365 | locationAccuracyCircle?.let{ 366 | it.center = latLng 367 | } ?: run{ 368 | this.locationAccuracyCircle = map?.addCircle( 369 | CircleOptions() 370 | .center(latLng) 371 | .fillColor(Color.argb(64, 0, 0, 0)) 372 | .strokeColor(Color.argb(64, 0, 0, 0)) 373 | .strokeWidth(0.0f) 374 | .radius(location.accuracy.toDouble()) 375 | ) //set radius to horizonal accuracy in meter. 376 | } 377 | } 378 | 379 | 380 | private fun addPolyline() { 381 | locationService?.locationList?.let{locationList -> 382 | 383 | runningPathPolyline?.let{ 384 | val toLocation = locationList[locationList.size - 1] 385 | val to = LatLng( 386 | toLocation.latitude, 387 | toLocation.longitude 388 | ) 389 | val points = it.points 390 | points.add(to) 391 | it.points = points 392 | } ?: run{ 393 | if (locationList.size > 1) { 394 | val fromLocation = locationList[locationList.size - 2] 395 | val toLocation = locationList[locationList.size - 1] 396 | 397 | val from = LatLng( 398 | fromLocation.latitude, 399 | fromLocation.longitude 400 | ) 401 | 402 | val to = LatLng( 403 | toLocation.latitude, 404 | toLocation.longitude 405 | ) 406 | 407 | this.runningPathPolyline = map?.addPolyline( 408 | polylineOptions 409 | .add(from, to) 410 | .width(polylineWidth.toFloat()).color(Color.parseColor("#801B60FE")).geodesic(true) 411 | ) 412 | } 413 | } 414 | } 415 | 416 | } 417 | 418 | private fun clearPolyline() { 419 | runningPathPolyline?.remove() 420 | runningPathPolyline = null 421 | } 422 | 423 | /* Filter Visualization */ 424 | private fun drawMalLocations() { 425 | locationService?.let{ 426 | drawMalMarkers(it.oldLocationList, oldLocationMarkerBitmapDescriptor!!) 427 | drawMalMarkers(it.noAccuracyLocationList, noAccuracyLocationMarkerBitmapDescriptor!!) 428 | drawMalMarkers(it.inaccurateLocationList, inaccurateLocationMarkerBitmapDescriptor!!) 429 | drawMalMarkers(it.kalmanNGLocationList, kalmanNGLocationMarkerBitmapDescriptor!!) 430 | } 431 | } 432 | 433 | private fun drawMalMarkers(locationList: ArrayList, descriptor: BitmapDescriptor) { 434 | for (location in locationList) { 435 | val latLng = LatLng(location.latitude, location.longitude) 436 | val marker = map?.addMarker( 437 | MarkerOptions() 438 | .position(latLng) 439 | .flat(true) 440 | .anchor(0.5f, 0.5f) 441 | .icon(descriptor) 442 | ) 443 | marker?.let{ 444 | malMarkers.add(it) 445 | } 446 | } 447 | } 448 | 449 | private fun drawPredictionRange(location: Location) { 450 | val latLng = LatLng(location.latitude, location.longitude) 451 | 452 | predictionRange?.let{ 453 | it.center = latLng 454 | } ?: run { 455 | predictionRange = map?.addCircle( 456 | CircleOptions() 457 | .center(latLng) 458 | .fillColor(Color.argb(50, 30, 207, 0)) 459 | .strokeColor(Color.argb(128, 30, 207, 0)) 460 | .strokeWidth(1.0f) 461 | .radius(30.0) 462 | ) //30 meters of the prediction range 463 | } 464 | 465 | this.predictionRange?.isVisible = true 466 | Handler(Looper.getMainLooper()).postDelayed({ 467 | this@MainActivity.predictionRange?.isVisible = false 468 | }, 2000) 469 | } 470 | 471 | private fun clearMalMarkers() { 472 | for (marker in malMarkers) { 473 | marker.remove() 474 | } 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/inaccurate_location_marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/drawable-xxhdpi/inaccurate_location_marker.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/kalman_ng_location_marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/drawable-xxhdpi/kalman_ng_location_marker.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/no_accuracy_location_marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/drawable-xxhdpi/no_accuracy_location_marker.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/old_location_marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/drawable-xxhdpi/old_location_marker.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/run_start_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/drawable-xxhdpi/run_start_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/run_stop_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/drawable-xxhdpi/run_stop_button.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/user_position_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/drawable-xxhdpi/user_position_point.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_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/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 15 | 16 | 17 | 24 | 25 | 33 | 34 | 35 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | #FFFFFF 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidLocationStarterKitInKotlin 3 | AndroidLocationStarterKit 4 | 5 | Tracking location… 6 | Notification about location tracking state 7 | Shown while tracking user\'s location 8 | 9 | Please authorize the app to use location service 10 | Please choose \"While using the app\" in the next dialog to authorize the app to tracks your location. 11 | next 12 | App isn\'t not given proper permission to use Location Service 13 | The app needs Location Service permission to track your position. 14 | Settings 15 | 16 | I don\'t permit 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/test/java/com/goldrushcomputing/androidlocationstarterkitinkotlin/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.goldrushcomputing.androidlocationstarterkitinkotlin 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | id 'com.android.application' version '7.1.3' apply false 4 | id 'com.android.library' version '7.1.3' apply false 5 | id 'org.jetbrains.kotlin.android' version '1.6.20' apply false 6 | } 7 | 8 | task clean(type: Delete) { 9 | delete rootProject.buildDir 10 | } 11 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizutori/AndroidLocationStarterKitInKotlin/dc1c138d672be3a93d71714d11e4e5a4724d182c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jun 01 11:06:58 JST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "AndroidLocationStarterKitInKotlin" 16 | include ':app' 17 | --------------------------------------------------------------------------------