Add launch stats reporting per callsign
При старте приложения обновляет запись своего позывного во внешнем документе: версия ПО, время последнего запуска и число запусков. Записи других позывных не изменяются.
This commit is contained in:
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: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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ class SvxApp : Application() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
createPushChannel()
|
createPushChannel()
|
||||||
initRuStorePush()
|
initRuStorePush()
|
||||||
|
StatsReporter.report(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createPushChannel() {
|
private fun createPushChannel() {
|
||||||
|
|||||||
Reference in New Issue
Block a user