Compare commits
10 Commits
a3ff36ba67
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
386fb20bff
|
|||
|
2f4cfa33ad
|
|||
|
5f7d4c0163
|
|||
|
4e517b87cd
|
|||
|
831b6bf491
|
|||
|
515f950bde
|
|||
|
761bd921ab
|
|||
|
7bee8910ec
|
|||
|
d6f352be29
|
|||
|
52816c155b
|
+20
-3
@@ -3,6 +3,8 @@
|
||||
* Copyright (c) 2025. All rights reserved.
|
||||
*/
|
||||
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
@@ -15,12 +17,22 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "ru.risdeveau.geotracker"
|
||||
minSdk = 24
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
versionCode = 3
|
||||
versionName = "1.2"
|
||||
setProperty("archivesBaseName", "$applicationId-v$versionCode($versionName)")
|
||||
|
||||
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 {
|
||||
@@ -30,6 +42,10 @@ android {
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
manifestPlaceholders["sentry_env"] = "production"
|
||||
}
|
||||
debug {
|
||||
manifestPlaceholders["sentry_env"] = "develop"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
@@ -71,4 +87,5 @@ dependencies {
|
||||
implementation(libs.logback.classic)
|
||||
|
||||
implementation(libs.splitties.base)
|
||||
implementation(libs.sentry)
|
||||
}
|
||||
@@ -26,6 +26,35 @@
|
||||
android:theme="@style/Theme.GeoTracker"
|
||||
android:usesCleartextTraffic="true"
|
||||
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
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -48,11 +48,11 @@ suspend fun health(baseurl: String): Boolean {
|
||||
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)
|
||||
json.put("lon", data.ln)
|
||||
json.put("lat", data.lt)
|
||||
json.put("user_name", data.nick)
|
||||
|
||||
client.post("$baseurl/map") {
|
||||
client.post("$baseurl/api/app") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(json.toString(2))
|
||||
}
|
||||
|
||||
@@ -8,6 +8,13 @@ package ru.risdeveau.geotracker
|
||||
import splitties.preferences.Preferences
|
||||
|
||||
object SettingsPreferences : Preferences("settings") {
|
||||
var username by stringPref("username", "anonimous")
|
||||
var url by stringPref("url", "https://example.com")
|
||||
var username by stringPref("username", "")
|
||||
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)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -15,6 +16,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
|
||||
|
||||
class LocationForegroundService : Service() {
|
||||
@@ -35,18 +37,26 @@ class LocationForegroundService : Service() {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
if (
|
||||
!(hasPermission(ACCESS_FINE_LOCATION)
|
||||
&& (
|
||||
(Build.VERSION.SDK_INT < 33)
|
||||
|| hasPermission(POST_NOTIFICATIONS)
|
||||
)
|
||||
)) {
|
||||
))
|
||||
|| intent?.action == ACTION_STOP_SERVICE
|
||||
) {
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
@@ -55,35 +65,58 @@ class LocationForegroundService : Service() {
|
||||
val notification = createNotification()
|
||||
startForeground(1, notification)
|
||||
|
||||
locationTracker.startTracking(5000)
|
||||
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")
|
||||
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)
|
||||
}
|
||||
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("Обновление каждые 5 секунд")
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.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"
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Done
|
||||
import androidx.compose.material3.Button
|
||||
@@ -33,6 +34,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -40,6 +42,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -47,6 +50,7 @@ import ru.risdeveau.geotracker.ui.theme.GeoTrackerTheme
|
||||
import splitties.experimental.ExperimentalSplittiesApi
|
||||
import splitties.init.appCtx
|
||||
import splitties.resources.appStr
|
||||
import java.time.Instant
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
@@ -65,13 +69,29 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
when (screen) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Text("Hello world")
|
||||
Column(Modifier.align(Alignment.Center)) {
|
||||
Text("Всего отправлено: $totalSent")
|
||||
Text("Отправлено за эту сессию: $sessionSent")
|
||||
Text("В последний раз было отпавлено $lastSent секунд назад")
|
||||
}
|
||||
}
|
||||
|
||||
Screen.Settings -> {
|
||||
@@ -89,6 +109,7 @@ class MainActivity : ComponentActivity() {
|
||||
launch {
|
||||
screen = if (
|
||||
health(SettingsPreferences.url)
|
||||
&& SettingsPreferences.username.isNotBlank()
|
||||
&& hasPermission(ACCESS_FINE_LOCATION)
|
||||
&& (
|
||||
(Build.VERSION.SDK_INT < 33)
|
||||
@@ -121,6 +142,7 @@ sealed class Screen {
|
||||
fun Settings(modifier: Modifier = Modifier, onConfirm: () -> Unit) {
|
||||
var username by remember { mutableStateOf(SettingsPreferences.username) }
|
||||
var url by remember { mutableStateOf(SettingsPreferences.url) }
|
||||
var interval by remember { mutableIntStateOf(SettingsPreferences.interval) }
|
||||
var urlIsValid by remember { mutableStateOf(false) }
|
||||
var loading by remember { mutableStateOf(false) }
|
||||
var fineLoc by remember { mutableStateOf(hasPermission(ACCESS_FINE_LOCATION)) }
|
||||
@@ -162,6 +184,17 @@ fun Settings(modifier: Modifier = Modifier, onConfirm: () -> Unit) {
|
||||
}
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
@@ -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"
|
||||
composeBom = "2024.04.01"
|
||||
logbackClassic = "1.2.11"
|
||||
sentry = "8.12.0"
|
||||
splittiesFunPackAndroidBase = "3.0.0"
|
||||
|
||||
[libraries]
|
||||
@@ -35,6 +36,7 @@ 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" }
|
||||
sentry = { module = "io.sentry:sentry-android", version.ref = "sentry" }
|
||||
splitties-base = { module = "com.louiscad.splitties:splitties-fun-pack-android-base", version.ref = "splittiesFunPackAndroidBase" }
|
||||
|
||||
[plugins]
|
||||
|
||||
Reference in New Issue
Block a user