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" }