feat: add telemetry + bump to 1.9.9
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import com.a2i.forwarder.core.ConnectivityMonitor
|
||||
import com.a2i.forwarder.core.LogStore
|
||||
import com.a2i.forwarder.core.PhoneCallMonitor
|
||||
import com.a2i.forwarder.core.SmsFallbackForwarder
|
||||
import com.a2i.forwarder.core.Telemetry
|
||||
import com.a2i.forwarder.core.UpdateChecker
|
||||
import com.a2i.forwarder.store.AppRulesStore
|
||||
import com.a2i.forwarder.store.SettingsStore
|
||||
@@ -33,6 +34,8 @@ class A2iApp : Application() {
|
||||
private set
|
||||
lateinit var updateChecker: UpdateChecker
|
||||
private set
|
||||
lateinit var telemetry: Telemetry
|
||||
private set
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -48,6 +51,8 @@ class A2iApp : Application() {
|
||||
smsForwarder = SmsFallbackForwarder(this)
|
||||
updateChecker = UpdateChecker(this)
|
||||
updateChecker.start()
|
||||
telemetry = Telemetry(this, appScope)
|
||||
telemetry.start()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.a2i.forwarder.core
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONObject
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class Telemetry(
|
||||
private val context: Context,
|
||||
private val scope: CoroutineScope,
|
||||
) {
|
||||
private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
private val http = OkHttpClient.Builder()
|
||||
.connectTimeout(8, TimeUnit.SECONDS)
|
||||
.writeTimeout(8, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.retryOnConnectionFailure(true)
|
||||
.build()
|
||||
private val sessionId = UUID.randomUUID().toString()
|
||||
|
||||
fun start() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (!prefs.getBoolean(KEY_INSTALL_SENT, false)) {
|
||||
send("install").onSuccess {
|
||||
prefs.edit().putBoolean(KEY_INSTALL_SENT, true).apply()
|
||||
}
|
||||
}
|
||||
send("launch")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun send(event: String): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val payload = basePayload(event).toString().toRequestBody(JSON)
|
||||
val request = Request.Builder()
|
||||
.url(EVENTS_URL)
|
||||
.header("User-Agent", userAgent())
|
||||
.post(payload)
|
||||
.build()
|
||||
http.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) error("HTTP ${response.code}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun basePayload(event: String): JSONObject {
|
||||
val version = appVersion()
|
||||
val locale = Locale.getDefault()
|
||||
return JSONObject()
|
||||
.put("app_id", APP_ID)
|
||||
.put("app_name", APP_NAME)
|
||||
.put("event", event)
|
||||
.put("install_id", installId())
|
||||
.put("device_id", androidId())
|
||||
.put("session_id", sessionId)
|
||||
.put("platform", "android")
|
||||
.put("package_name", context.packageName)
|
||||
.put("app_version", version.name)
|
||||
.put("app_build", version.code)
|
||||
.put("os_version", "Android ${Build.VERSION.RELEASE} SDK ${Build.VERSION.SDK_INT}")
|
||||
.put("manufacturer", Build.MANUFACTURER.orEmpty())
|
||||
.put("model", Build.MODEL.orEmpty())
|
||||
.put("locale", locale.toLanguageTag())
|
||||
.put("timezone", TimeZone.getDefault().id)
|
||||
.put("country", locale.country.orEmpty())
|
||||
.put("network_type", networkType())
|
||||
.put("user_agent", userAgent())
|
||||
.put("source", "android-app")
|
||||
}
|
||||
|
||||
private fun installId(): String {
|
||||
prefs.getString(KEY_INSTALL_ID, null)?.let { return it }
|
||||
val id = UUID.randomUUID().toString()
|
||||
prefs.edit().putString(KEY_INSTALL_ID, id).apply()
|
||||
return id
|
||||
}
|
||||
|
||||
private fun androidId(): String =
|
||||
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID).orEmpty()
|
||||
|
||||
private fun appVersion(): AppVersion {
|
||||
val info = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
return AppVersion(
|
||||
name = info.versionName ?: "0",
|
||||
code = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
info.longVersionCode.toString()
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
info.versionCode.toString()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun networkType(): String {
|
||||
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return "unknown"
|
||||
val network = manager.activeNetwork ?: return "none"
|
||||
val capabilities = manager.getNetworkCapabilities(network) ?: return "unknown"
|
||||
return when {
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "wifi"
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> "cellular"
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> "ethernet"
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> "vpn"
|
||||
else -> "other"
|
||||
}
|
||||
}
|
||||
|
||||
private fun userAgent(): String {
|
||||
val version = appVersion()
|
||||
return "GotMsg/${version.name} (${context.packageName}; Android ${Build.VERSION.SDK_INT}; ${Build.MANUFACTURER} ${Build.MODEL})"
|
||||
}
|
||||
|
||||
private data class AppVersion(
|
||||
val name: String,
|
||||
val code: String,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val APP_ID = "gotmsg"
|
||||
private const val APP_NAME = "GotMsg"
|
||||
private const val EVENTS_URL = "https://stats.songer.eu.org/v1/events"
|
||||
private const val PREFS_NAME = "gotmsg_telemetry"
|
||||
private const val KEY_INSTALL_ID = "install_id"
|
||||
private const val KEY_INSTALL_SENT = "install_sent"
|
||||
private val JSON = "application/json; charset=utf-8".toMediaType()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user