diff --git a/app/src/main/java/ru/ua1zbe/svxremote/StatsReporter.kt b/app/src/main/java/ru/ua1zbe/svxremote/StatsReporter.kt new file mode 100644 index 0000000..1ead4c2 --- /dev/null +++ b/app/src/main/java/ru/ua1zbe/svxremote/StatsReporter.kt @@ -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() + } + } +} diff --git a/app/src/main/java/ru/ua1zbe/svxremote/SvxApp.kt b/app/src/main/java/ru/ua1zbe/svxremote/SvxApp.kt index ad07a20..8906543 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/SvxApp.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/SvxApp.kt @@ -17,6 +17,7 @@ class SvxApp : Application() { super.onCreate() createPushChannel() initRuStorePush() + StatsReporter.report(this) } private fun createPushChannel() {