Compare commits
13 Commits
5d81e98b02
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
386fb20bff
|
|||
|
2f4cfa33ad
|
|||
|
5f7d4c0163
|
|||
|
4e517b87cd
|
|||
|
831b6bf491
|
|||
|
515f950bde
|
|||
|
761bd921ab
|
|||
|
7bee8910ec
|
|||
|
d6f352be29
|
|||
|
52816c155b
|
|||
|
a3ff36ba67
|
|||
|
8a54db266f
|
|||
|
db766c2acc
|
+21
-3
@@ -3,6 +3,8 @@
|
|||||||
* Copyright (c) 2025. All rights reserved.
|
* Copyright (c) 2025. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
@@ -15,12 +17,22 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "ru.risdeveau.geotracker"
|
applicationId = "ru.risdeveau.geotracker"
|
||||||
minSdk = 24
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 3
|
||||||
versionName = "1.0"
|
versionName = "1.2"
|
||||||
|
setProperty("archivesBaseName", "$applicationId-v$versionCode($versionName)")
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
val secretProperties = Properties().apply {
|
||||||
|
val secretFile = rootProject.file("secrets.properties")
|
||||||
|
if (secretFile.exists())
|
||||||
|
secretFile.inputStream().use { load(it) }
|
||||||
|
else
|
||||||
|
println("Warning: secrets.properties not found!")
|
||||||
|
}
|
||||||
|
manifestPlaceholders["sentry_url"] = secretProperties.getProperty("SENTRY_URL")!!
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -30,6 +42,10 @@ android {
|
|||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
|
manifestPlaceholders["sentry_env"] = "production"
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
manifestPlaceholders["sentry_env"] = "develop"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -67,7 +83,9 @@ dependencies {
|
|||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.ktor.client.okhttp)
|
implementation(libs.ktor.client.okhttp)
|
||||||
implementation(libs.ktor.client.logging)
|
implementation(libs.ktor.client.logging)
|
||||||
|
implementation(libs.ktor.client.json)
|
||||||
implementation(libs.logback.classic)
|
implementation(libs.logback.classic)
|
||||||
|
|
||||||
implementation(libs.splitties.base)
|
implementation(libs.splitties.base)
|
||||||
|
implementation(libs.sentry)
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,14 @@
|
|||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -17,7 +24,37 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.GeoTracker"
|
android:theme="@style/Theme.GeoTracker"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="io.sentry.dsn"
|
||||||
|
android:value="${sentry_url}" />
|
||||||
|
<meta-data
|
||||||
|
android:name="io.sentry.environment"
|
||||||
|
android:value="${sentry_env}" />
|
||||||
|
<meta-data
|
||||||
|
android:name="io.sentry.send-default-pii"
|
||||||
|
android:value="true" />
|
||||||
|
<meta-data
|
||||||
|
android:name="io.sentry.traces.sample-rate"
|
||||||
|
android:value="1.0" />
|
||||||
|
<meta-data
|
||||||
|
android:name="io.sentry.traces.user-interaction.enable"
|
||||||
|
android:value="true" />
|
||||||
|
<meta-data
|
||||||
|
android:name="io.sentry.attach-screenshot"
|
||||||
|
android:value="true" />
|
||||||
|
<meta-data
|
||||||
|
android:name="io.sentry.attach-view-hierarchy"
|
||||||
|
android:value="true" />
|
||||||
|
<meta-data
|
||||||
|
android:name="io.sentry.session-replay.on-error-sample-rate"
|
||||||
|
android:value="1.0" />
|
||||||
|
<meta-data
|
||||||
|
android:name="io.sentry.session-replay.session-sample-rate"
|
||||||
|
android:value="0.1" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -29,6 +66,12 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".LocationForegroundService"
|
||||||
|
android:foregroundServiceType="location"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -9,7 +9,12 @@ import io.ktor.client.*
|
|||||||
import io.ktor.client.engine.okhttp.OkHttp
|
import io.ktor.client.engine.okhttp.OkHttp
|
||||||
import io.ktor.client.plugins.logging.*
|
import io.ktor.client.plugins.logging.*
|
||||||
import io.ktor.client.request.get
|
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.HttpStatusCode
|
||||||
|
import io.ktor.http.contentType
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
val client = HttpClient(OkHttp) {
|
val client = HttpClient(OkHttp) {
|
||||||
install(Logging) {
|
install(Logging) {
|
||||||
@@ -38,6 +43,22 @@ suspend fun health(baseurl: String): Boolean {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Send data to a server
|
* 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("lon", data.ln)
|
||||||
|
json.put("lat", data.lt)
|
||||||
|
json.put("user_name", data.nick)
|
||||||
|
|
||||||
|
client.post("$baseurl/api/app") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(json.toString(2))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Error: ${e.message}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,13 @@ package ru.risdeveau.geotracker
|
|||||||
import splitties.preferences.Preferences
|
import splitties.preferences.Preferences
|
||||||
|
|
||||||
object SettingsPreferences : Preferences("settings") {
|
object SettingsPreferences : Preferences("settings") {
|
||||||
var username by stringPref("username", "anonimous")
|
var username by stringPref("username", "")
|
||||||
var url by stringPref("url", "https://example.com")
|
var url by stringPref("url", "https://geo.tmp.codrs.ru")
|
||||||
|
val interval by IntPref("interval", 15)
|
||||||
|
}
|
||||||
|
|
||||||
|
object StatisticsPreferences : Preferences("statistics") {
|
||||||
|
var totalSent by IntPref("total_sent", 0)
|
||||||
|
var sessionSent by IntPref("session_sent", 0)
|
||||||
|
var lastSent by LongPref("last_sent", 0)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package ru.risdeveau.geotracker
|
||||||
|
|
||||||
|
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
import android.Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
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
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
StatisticsPreferences.apply {
|
||||||
|
totalSent++
|
||||||
|
sessionSent++
|
||||||
|
lastSent = Instant.now().epochSecond
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
Log.d("Service", "onStartCommand")
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(hasPermission(ACCESS_FINE_LOCATION)
|
||||||
|
&& (
|
||||||
|
(Build.VERSION.SDK_INT < 33)
|
||||||
|
|| hasPermission(POST_NOTIFICATIONS)
|
||||||
|
))
|
||||||
|
|| intent?.action == ACTION_STOP_SERVICE
|
||||||
|
) {
|
||||||
|
stopSelf()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotificationChannel()
|
||||||
|
val notification = createNotification()
|
||||||
|
startForeground(1, notification)
|
||||||
|
|
||||||
|
locationTracker.startTracking(SettingsPreferences.interval * 1000L)
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
Log.d("Service", "Destroyed")
|
||||||
|
locationTracker.stopTracking()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotificationChannel() {
|
||||||
|
Log.d("Service", "createNotificationChannel")
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
"location_channel",
|
||||||
|
"Отправка Местоположения",
|
||||||
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
)
|
||||||
|
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
manager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotification(): Notification {
|
||||||
|
val stopIntent = Intent(this, LocationForegroundService::class.java).apply {
|
||||||
|
action = ACTION_STOP_SERVICE
|
||||||
|
}
|
||||||
|
|
||||||
|
val stopPendingIntent = PendingIntent.getService(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
stopIntent,
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
||||||
|
else
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
|
||||||
|
return NotificationCompat.Builder(this, "location_channel")
|
||||||
|
.setContentTitle("Отслеживание местоположения")
|
||||||
|
.setContentText("Обновление каждые ${SettingsPreferences.interval} секунд")
|
||||||
|
.setSmallIcon(R.drawable.share_location)
|
||||||
|
.addAction(
|
||||||
|
R.drawable.cancel,
|
||||||
|
"Остановить",
|
||||||
|
stopPendingIntent
|
||||||
|
)
|
||||||
|
.setOngoing(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTION_STOP_SERVICE = "ru.risdeveau.geotracker.STOP_SERVICE"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,18 +5,28 @@
|
|||||||
|
|
||||||
package ru.risdeveau.geotracker
|
package ru.risdeveau.geotracker
|
||||||
|
|
||||||
|
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
import android.Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Done
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@@ -24,26 +34,29 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.intellij.lang.annotations.JdkConstants
|
|
||||||
import ru.risdeveau.geotracker.ui.theme.GeoTrackerTheme
|
import ru.risdeveau.geotracker.ui.theme.GeoTrackerTheme
|
||||||
import splitties.resources.appColor
|
import splitties.experimental.ExperimentalSplittiesApi
|
||||||
|
import splitties.init.appCtx
|
||||||
import splitties.resources.appStr
|
import splitties.resources.appStr
|
||||||
import kotlin.apply
|
import java.time.Instant
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
GeoTrackerTheme {
|
GeoTrackerTheme {
|
||||||
@@ -55,10 +68,36 @@ class MainActivity : ComponentActivity() {
|
|||||||
var screen by remember { mutableStateOf<Screen>(Screen.Loading) }
|
var screen by remember { mutableStateOf<Screen>(Screen.Loading) }
|
||||||
|
|
||||||
when (screen) {
|
when (screen) {
|
||||||
Screen.Main -> TODO()
|
Screen.Main -> {
|
||||||
|
var totalSent by remember { mutableIntStateOf(0) }
|
||||||
|
var sessionSent by remember { mutableIntStateOf(0) }
|
||||||
|
var lastSent by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
Log.d("Thread", "Starting...")
|
||||||
|
startLocationService()
|
||||||
|
StatisticsPreferences.sessionSent = 0
|
||||||
|
Log.d("Thread", "Started")
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
totalSent = StatisticsPreferences.totalSent
|
||||||
|
sessionSent = StatisticsPreferences.sessionSent
|
||||||
|
lastSent = (Instant.now().epochSecond - StatisticsPreferences.lastSent).toInt()
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
|
Text("Всего отправлено: $totalSent")
|
||||||
|
Text("Отправлено за эту сессию: $sessionSent")
|
||||||
|
Text("В последний раз было отпавлено $lastSent секунд назад")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Screen.Settings -> {
|
Screen.Settings -> {
|
||||||
Settings(Modifier.align(Alignment.Center))
|
Settings(Modifier.align(Alignment.Center)) {
|
||||||
|
screen = Screen.Main
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Screen.Loading -> {
|
Screen.Loading -> {
|
||||||
@@ -68,7 +107,15 @@ class MainActivity : ComponentActivity() {
|
|||||||
CircularProgressIndicator(Modifier.align(Alignment.Center))
|
CircularProgressIndicator(Modifier.align(Alignment.Center))
|
||||||
LaunchedEffect(true) {
|
LaunchedEffect(true) {
|
||||||
launch {
|
launch {
|
||||||
screen = if (health(SettingsPreferences.url))
|
screen = if (
|
||||||
|
health(SettingsPreferences.url)
|
||||||
|
&& SettingsPreferences.username.isNotBlank()
|
||||||
|
&& hasPermission(ACCESS_FINE_LOCATION)
|
||||||
|
&& (
|
||||||
|
(Build.VERSION.SDK_INT < 33)
|
||||||
|
|| hasPermission(POST_NOTIFICATIONS)
|
||||||
|
)
|
||||||
|
)
|
||||||
Screen.Main
|
Screen.Main
|
||||||
else
|
else
|
||||||
Screen.Settings
|
Screen.Settings
|
||||||
@@ -90,12 +137,21 @@ sealed class Screen {
|
|||||||
object Loading : Screen()
|
object Loading : Screen()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSplittiesApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Settings(modifier: Modifier = Modifier) {
|
fun Settings(modifier: Modifier = Modifier, onConfirm: () -> Unit) {
|
||||||
var username by remember { mutableStateOf("") }
|
var username by remember { mutableStateOf(SettingsPreferences.username) }
|
||||||
var url by remember { mutableStateOf("") }
|
var url by remember { mutableStateOf(SettingsPreferences.url) }
|
||||||
|
var interval by remember { mutableIntStateOf(SettingsPreferences.interval) }
|
||||||
var urlIsValid by remember { mutableStateOf(false) }
|
var urlIsValid by remember { mutableStateOf(false) }
|
||||||
var loading by remember { mutableStateOf(false) }
|
var loading by remember { mutableStateOf(false) }
|
||||||
|
var fineLoc by remember { mutableStateOf(hasPermission(ACCESS_FINE_LOCATION)) }
|
||||||
|
var notifications by remember { mutableStateOf(
|
||||||
|
if (Build.VERSION.SDK_INT >= 33) {
|
||||||
|
hasPermission(POST_NOTIFICATIONS)
|
||||||
|
} else true
|
||||||
|
) }
|
||||||
|
val hasPerms = fineLoc && notifications
|
||||||
|
|
||||||
LaunchedEffect(url) {
|
LaunchedEffect(url) {
|
||||||
if (url.isNotEmpty()) {
|
if (url.isNotEmpty()) {
|
||||||
@@ -116,13 +172,86 @@ fun Settings(modifier: Modifier = Modifier) {
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = url,
|
value = url,
|
||||||
onValueChange = { url = it },
|
onValueChange = { url = it },
|
||||||
placeholder = { Text("https://geo.example.com", style = TextStyle(color = MaterialTheme.colorScheme.onSurfaceVariant)) },
|
placeholder = {
|
||||||
|
Text("https://geo.example.com",
|
||||||
|
style = TextStyle(color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
)
|
||||||
|
},
|
||||||
label = { Text(appStr(R.string.server_url)) },
|
label = { Text(appStr(R.string.server_url)) },
|
||||||
trailingIcon = { if (loading) CircularProgressIndicator() }
|
trailingIcon = {
|
||||||
|
if (loading) CircularProgressIndicator()
|
||||||
|
else if (urlIsValid) Icon(Icons.Outlined.Done, "Done")
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Button({}, enabled = urlIsValid) {
|
OutlinedTextField(
|
||||||
|
value = interval.toString(),
|
||||||
|
onValueChange = {
|
||||||
|
val newVal = it.toIntOrNull()
|
||||||
|
if (newVal != null)
|
||||||
|
interval = newVal.coerceIn(1..300)
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
label = { Text("Интервал отправки") }
|
||||||
|
)
|
||||||
|
|
||||||
|
GetPermission(ACCESS_FINE_LOCATION) { fineLoc = true; }
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 33)
|
||||||
|
GetPermission(POST_NOTIFICATIONS) { notifications = true; }
|
||||||
|
|
||||||
|
Button({
|
||||||
|
SettingsPreferences.username = username.trim()
|
||||||
|
SettingsPreferences.url = url
|
||||||
|
onConfirm()
|
||||||
|
}, enabled = urlIsValid
|
||||||
|
&& !loading
|
||||||
|
&& username.trim().isNotEmpty()
|
||||||
|
&& hasPerms
|
||||||
|
) {
|
||||||
Text(appStr(R.string.apply))
|
Text(appStr(R.string.apply))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GetPermission(permission: String, onSuccess: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var hasPerm by remember { mutableStateOf(
|
||||||
|
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
|
||||||
|
)}
|
||||||
|
|
||||||
|
val launcher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { granted -> hasPerm = granted }
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { launcher.launch(permission) },
|
||||||
|
enabled = !hasPerm
|
||||||
|
) {
|
||||||
|
if (hasPerm) {
|
||||||
|
onSuccess()
|
||||||
|
Icon(Icons.Outlined.Done, "Done")
|
||||||
|
Text("Разрешение получено")
|
||||||
|
} else {
|
||||||
|
Text("Получить разрешение")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasPermission(permission: String): Boolean {
|
||||||
|
return ContextCompat.checkSelfPermission(
|
||||||
|
appCtx,
|
||||||
|
permission
|
||||||
|
) == 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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M336,680L480,536L624,680L680,624L536,480L680,336L624,280L480,424L336,280L280,336L424,480L280,624L336,680ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M519,878L519,798Q561,792 600.5,775Q640,758 675,732L733,790Q686,827 632,849.5Q578,872 519,878ZM789,732L733,676Q759,643 775,603.5Q791,564 797,520L879,520Q871,582 848.5,635.5Q826,689 789,732ZM797,440Q791,395 775,355.5Q759,316 733,284L789,228Q827,272 850.5,326Q874,380 879,440L797,440ZM439,878Q286,860 183.5,747Q81,634 81,480Q81,325 183.5,212Q286,99 439,82L439,162Q319,179 240,269Q161,359 161,480Q161,601 240,690.5Q319,780 439,798L439,878ZM677,228Q641,201 601,184Q561,167 519,162L519,82Q578,87 632,109.5Q686,132 733,170L677,228ZM480,680Q422,631 371,575Q320,519 320,444Q320,376 366.5,328Q413,280 480,280Q547,280 593.5,328Q640,376 640,444Q640,519 589,575Q538,631 480,680ZM480,480Q498,480 510.5,467.5Q523,455 523,437Q523,420 510.5,407Q498,394 480,394Q462,394 449.5,407Q437,420 437,437Q437,455 449.5,467.5Q462,480 480,480Z"/>
|
||||||
|
</vector>
|
||||||
@@ -11,6 +11,7 @@ lifecycleRuntimeKtx = "2.8.7"
|
|||||||
activityCompose = "1.10.1"
|
activityCompose = "1.10.1"
|
||||||
composeBom = "2024.04.01"
|
composeBom = "2024.04.01"
|
||||||
logbackClassic = "1.2.11"
|
logbackClassic = "1.2.11"
|
||||||
|
sentry = "8.12.0"
|
||||||
splittiesFunPackAndroidBase = "3.0.0"
|
splittiesFunPackAndroidBase = "3.0.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
@@ -31,9 +32,11 @@ 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-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
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-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
|
||||||
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", 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" }
|
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logbackClassic" }
|
||||||
|
sentry = { module = "io.sentry:sentry-android", version.ref = "sentry" }
|
||||||
splitties-base = { module = "com.louiscad.splitties:splitties-fun-pack-android-base", version.ref = "splittiesFunPackAndroidBase" }
|
splitties-base = { module = "com.louiscad.splitties:splitties-fun-pack-android-base", version.ref = "splittiesFunPackAndroidBase" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|||||||
Reference in New Issue
Block a user