diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index ed118fc..5db132f 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -67,6 +67,7 @@ dependencies {
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.logging)
+ implementation(libs.ktor.client.json)
implementation(libs.logback.classic)
implementation(libs.splitties.base)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e2b8fbe..1e1041a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,6 +11,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/ru/risdeveau/geotracker/API.kt b/app/src/main/java/ru/risdeveau/geotracker/API.kt
index dc63156..daee50c 100644
--- a/app/src/main/java/ru/risdeveau/geotracker/API.kt
+++ b/app/src/main/java/ru/risdeveau/geotracker/API.kt
@@ -9,7 +9,12 @@ import io.ktor.client.*
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.get
+import io.ktor.client.request.post
+import io.ktor.client.request.setBody
+import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
+import io.ktor.http.contentType
+import org.json.JSONObject
val client = HttpClient(OkHttp) {
install(Logging) {
@@ -38,6 +43,22 @@ suspend fun health(baseurl: String): Boolean {
/**
* Send data to a server
+ * @return true if sent successfully
*/
-fun sendGeo(baseurl: String, data: GeoData) {
+suspend fun sendGeo(baseurl: String = SettingsPreferences.url, data: GeoData): Boolean {
+ try {
+ val json = JSONObject()
+ json.put("ln", data.ln)
+ json.put("lt", data.lt)
+ json.put("nick", data.nick)
+
+ client.post("$baseurl/map") {
+ contentType(ContentType.Application.Json)
+ setBody(json.toString(2))
+ }
+ return true
+ } catch (e: Exception) {
+ println("Error: ${e.message}")
+ return false
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/risdeveau/geotracker/LocationForegroundService.kt b/app/src/main/java/ru/risdeveau/geotracker/LocationForegroundService.kt
new file mode 100644
index 0000000..4c904c8
--- /dev/null
+++ b/app/src/main/java/ru/risdeveau/geotracker/LocationForegroundService.kt
@@ -0,0 +1,76 @@
+package ru.risdeveau.geotracker
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+
+class LocationForegroundService : Service() {
+
+ private lateinit var locationTracker: LocationTracker
+ private val serviceScope = CoroutineScope(Dispatchers.Main + Job())
+
+ override fun onCreate() {
+ Log.d("Service", "onCreate")
+ super.onCreate()
+ locationTracker = LocationTracker(this) { location ->
+ serviceScope.launch {
+ sendGeo(
+ data = GeoData(
+ lt = location.latitude,
+ ln = location.longitude,
+ nick = SettingsPreferences.username
+ )
+ )
+ }
+ }
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Log.d("Service", "onStartCommand")
+ createNotificationChannel()
+ val notification = createNotification()
+ startForeground(1, notification)
+
+ locationTracker.startTracking(5000)
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ locationTracker.stopTracking()
+ super.onDestroy()
+ }
+
+ private fun createNotificationChannel() {
+ Log.d("Service", "createNotificationChannel")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ "location_channel",
+ "Location Tracking",
+ NotificationManager.IMPORTANCE_LOW
+ )
+ val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ manager.createNotificationChannel(channel)
+ }
+ }
+
+ private fun createNotification(): Notification {
+ return NotificationCompat.Builder(this, "location_channel")
+ .setContentTitle("Отслеживание местоположения")
+ .setContentText("Обновление каждые 5 секунд")
+ .setSmallIcon(R.drawable.ic_launcher_foreground)
+ .build()
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/risdeveau/geotracker/LocationTracker.kt b/app/src/main/java/ru/risdeveau/geotracker/LocationTracker.kt
new file mode 100644
index 0000000..cdb8c50
--- /dev/null
+++ b/app/src/main/java/ru/risdeveau/geotracker/LocationTracker.kt
@@ -0,0 +1,62 @@
+package ru.risdeveau.geotracker
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Context.LOCATION_SERVICE
+import android.content.pm.PackageManager
+import android.location.Location
+import android.location.LocationListener
+import android.location.LocationManager
+import android.os.Bundle
+import android.os.Looper
+import android.util.Log
+import androidx.core.content.ContextCompat
+
+class LocationTracker(
+ private val context: Context,
+ private val onLocationUpdate: (Location) -> Unit
+) : LocationListener {
+
+ private val locationManager =
+ context.getSystemService(LOCATION_SERVICE) as LocationManager
+
+ @SuppressLint("MissingPermission")
+ fun startTracking(interval: Long = 5000, minDistance: Float = 0f) {
+ Log.d("Tracker", "perms: ${hasPermissions()}")
+ if (!hasPermissions()) return
+
+ locationManager.requestLocationUpdates(
+ LocationManager.GPS_PROVIDER,
+ interval,
+ minDistance,
+ this,
+ Looper.getMainLooper()
+ )
+
+ locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)?.let {
+ onLocationUpdate(it)
+ }
+ }
+
+ fun stopTracking() {
+ locationManager.removeUpdates(this)
+ }
+
+ override fun onLocationChanged(location: Location) {
+ onLocationUpdate(location)
+ }
+
+ override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
+ override fun onProviderEnabled(provider: String) {}
+ override fun onProviderDisabled(provider: String) {}
+
+ private fun hasPermissions(): Boolean {
+ return ContextCompat.checkSelfPermission(
+ context,
+ android.Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(
+ context,
+ android.Manifest.permission.ACCESS_COARSE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/risdeveau/geotracker/MainActivity.kt b/app/src/main/java/ru/risdeveau/geotracker/MainActivity.kt
index 38f5fbb..d3bfb31 100644
--- a/app/src/main/java/ru/risdeveau/geotracker/MainActivity.kt
+++ b/app/src/main/java/ru/risdeveau/geotracker/MainActivity.kt
@@ -7,8 +7,11 @@ package ru.risdeveau.geotracker
import android.Manifest
import android.content.Context
+import android.content.Intent
import android.content.pm.PackageManager
+import android.os.Build
import android.os.Bundle
+import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
@@ -46,8 +49,10 @@ import splitties.init.appCtx
import splitties.resources.appStr
class MainActivity : ComponentActivity() {
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+
enableEdgeToEdge()
setContent {
GeoTrackerTheme {
@@ -60,6 +65,12 @@ class MainActivity : ComponentActivity() {
when (screen) {
Screen.Main -> {
+ LaunchedEffect(Unit) {
+ Log.d("Thread", "Starting...")
+ startLocationService()
+ Log.d("Thread", "Started")
+ }
+
Text("Hello world")
}
@@ -101,8 +112,8 @@ sealed class Screen {
@OptIn(ExperimentalSplittiesApi::class)
@Composable
fun Settings(modifier: Modifier = Modifier, onConfirm: () -> Unit) {
- var username by remember { mutableStateOf("") }
- var url by remember { mutableStateOf("") }
+ var username by remember { mutableStateOf(SettingsPreferences.username) }
+ var url by remember { mutableStateOf(SettingsPreferences.url) }
var urlIsValid by remember { mutableStateOf(false) }
var loading by remember { mutableStateOf(false) }
@@ -196,4 +207,14 @@ fun hasLocationPermissions(context: Context): Boolean {
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
+}
+
+fun startLocationService() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ appCtx.startForegroundService(Intent(appCtx, LocationForegroundService::class.java))
+ Log.d("startLocationService", "startForegroundService")
+ } else {
+ appCtx.startService(Intent(appCtx, LocationForegroundService::class.java))
+ Log.d("startLocationService", "startService")
+ }
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 87d0963..b64a90c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -31,6 +31,7 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
+ktor-client-json = { module = "io.ktor:ktor-client-json", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logbackClassic" }