Add launch stats reporting per callsign

При старте приложения обновляет запись своего позывного во внешнем
документе: версия ПО, время последнего запуска и число запусков.
Записи других позывных не изменяются.
This commit is contained in:
ua1zbe
2026-10-09 13:51:37 +03:00
parent 0a24a6f6c0
commit b6e4fb7e3a
2 changed files with 100 additions and 0 deletions

View 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:ss'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}.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()
}
}
}

View File

@@ -17,6 +17,7 @@ class SvxApp : Application() {
super.onCreate() super.onCreate()
createPushChannel() createPushChannel()
initRuStorePush() initRuStorePush()
StatsReporter.report(this)
} }
private fun createPushChannel() { private fun createPushChannel() {