Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcaf7355b5 | ||
|
|
78bac27175 | ||
|
|
2d332f10bb | ||
|
|
96ab5c76e2 | ||
|
|
4e7bf86f37 | ||
|
|
b6e4fb7e3a |
@@ -11,8 +11,8 @@ android {
|
||||
applicationId = "ru.ua1zbe.svxremote"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 16
|
||||
versionName = "1.0.14"
|
||||
versionCode = 17
|
||||
versionName = "1.0.15"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -330,6 +330,12 @@ class MainActivity : Activity(), SvxService.Listener {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onBackPressed() {
|
||||
// Кнопка «Назад» сворачивает приложение в фон, а не закрывает его.
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
hideNodeOverlay()
|
||||
|
||||
@@ -16,6 +16,24 @@ class SplashActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Тап по иконке при уже запущенном приложении: система добавляет
|
||||
// SplashActivity поверх существующей задачи. Не создаём дубликат —
|
||||
// закрываемся и показываем текущий экран (как «развернуть», без splash).
|
||||
if (!isTaskRoot &&
|
||||
intent?.action == Intent.ACTION_MAIN &&
|
||||
intent?.hasCategory(Intent.CATEGORY_LAUNCHER) == true
|
||||
) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// Засчитываем запуск только при холодном старте (процесс был выгружен).
|
||||
// Выход из фона или повторный тап по иконке (процесс жив) не считаем.
|
||||
if (SvxApp.coldStart) {
|
||||
SvxApp.coldStart = false
|
||||
StatsReporter.report(this)
|
||||
}
|
||||
if (mainAlive) {
|
||||
openMain()
|
||||
return
|
||||
|
||||
99
app/src/main/java/ru/ua1zbe/svxremote/StatsReporter.kt
Normal file
99
app/src/main/java/ru/ua1zbe/svxremote/StatsReporter.kt
Normal file
@@ -0,0 +1,99 @@
|
||||
package ru.ua1zbe.svxremote
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import org.json.JSONObject
|
||||
import ru.ua1zbe.svxremote.prefs.Prefs
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
/**
|
||||
* Учёт запусков по позывному.
|
||||
*
|
||||
* При старте приложения обновляет во внешнем документе запись своего позывного
|
||||
* (версия ПО, время последнего запуска, число запусков). Записи других
|
||||
* позывных не изменяются.
|
||||
*/
|
||||
object StatsReporter {
|
||||
|
||||
private const val TAG = "SVXRemote"
|
||||
private const val API =
|
||||
"https://git2.ua1zbe.ru/api/v1/repos/svxhomyak/svxx/contents/.svxstats.json"
|
||||
private const val TOKEN = "d1ef9a06c6471f009aefd3097772d263c267b008"
|
||||
private const val BRANCH = "main"
|
||||
|
||||
/** Запускает фоновое обновление статистики (не блокирует старт приложения). */
|
||||
fun report(context: Context) {
|
||||
val callsign = Prefs.load(context).callsign.trim().uppercase()
|
||||
if (callsign.isEmpty()) return
|
||||
Thread({
|
||||
repeat(3) { attempt ->
|
||||
if (runCatching { update(callsign) }.getOrDefault(false)) return@Thread
|
||||
Thread.sleep(1000L * (attempt + 1))
|
||||
}
|
||||
}, "svx-stats").apply { isDaemon = true }.start()
|
||||
}
|
||||
|
||||
private fun update(callsign: String): Boolean {
|
||||
var sha: String? = null
|
||||
val root = JSONObject()
|
||||
|
||||
val get = URL(API).openConnection() as HttpURLConnection
|
||||
try {
|
||||
get.setRequestProperty("Authorization", "token $TOKEN")
|
||||
get.connectTimeout = 10_000
|
||||
get.readTimeout = 10_000
|
||||
if (get.responseCode == 200) {
|
||||
val obj = JSONObject(get.inputStream.bufferedReader().readText())
|
||||
sha = obj.optString("sha").ifEmpty { null }
|
||||
val encoded = obj.optString("content", "").replace("\n", "")
|
||||
if (encoded.isNotEmpty()) {
|
||||
val decoded = String(Base64.decode(encoded, Base64.DEFAULT), Charsets.UTF_8)
|
||||
runCatching { JSONObject(decoded) }.getOrNull()?.let { parsed ->
|
||||
for (key in parsed.keys()) root.put(key, parsed.get(key))
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
get.disconnect()
|
||||
}
|
||||
|
||||
val now = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("Europe/Moscow")
|
||||
}.format(Date())
|
||||
val launches = (root.optJSONObject(callsign)?.optInt("launches", 0) ?: 0) + 1
|
||||
root.put(callsign, JSONObject().apply {
|
||||
put("callsign", callsign)
|
||||
put("version", BuildConfig.VERSION_NAME)
|
||||
put("lastLaunch", now)
|
||||
put("launches", launches)
|
||||
})
|
||||
|
||||
val body = JSONObject().apply {
|
||||
put("content", Base64.encodeToString(root.toString().toByteArray(), Base64.NO_WRAP))
|
||||
put("message", "stats")
|
||||
put("branch", BRANCH)
|
||||
if (sha != null) put("sha", sha)
|
||||
}
|
||||
val put = URL(API).openConnection() as HttpURLConnection
|
||||
return try {
|
||||
put.requestMethod = "PUT"
|
||||
put.doOutput = true
|
||||
put.setRequestProperty("Authorization", "token $TOKEN")
|
||||
put.setRequestProperty("Content-Type", "application/json")
|
||||
put.connectTimeout = 10_000
|
||||
put.readTimeout = 10_000
|
||||
put.outputStream.use { it.write(body.toString().toByteArray()) }
|
||||
val code = put.responseCode
|
||||
Log.i(TAG, "stats $callsign launches=$launches http=$code")
|
||||
code in 200..299
|
||||
} finally {
|
||||
put.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ class SvxApp : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// Процесс только что создан — значит приложение было выгружено и
|
||||
// запускается заново (а не выходит из фона/по иконке).
|
||||
coldStart = true
|
||||
createPushChannel()
|
||||
initRuStorePush()
|
||||
}
|
||||
@@ -59,5 +62,9 @@ class SvxApp : Application() {
|
||||
private const val TAG = "RuStorePush"
|
||||
private const val RUSTORE_PROJECT_ID = "Yd7tDjVDHqsnTJwHNYN08cYIvy8fsWU-"
|
||||
private const val PUSH_TOPIC = "svxhomyak"
|
||||
|
||||
/** true, если процесс создан заново (приложение было выгружено). */
|
||||
@Volatile
|
||||
var coldStart = false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user