Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70f9ba093f | |||
| f7adeda080 |
@@ -259,3 +259,5 @@ adb install -r app/build/outputs/apk/release/app-release.apk
|
|||||||
## License
|
## License
|
||||||
|
|
||||||
本项目仅供个人使用。
|
本项目仅供个人使用。
|
||||||
|
|
||||||
|
All Rights Reserved. 保留所有权利。
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ android {
|
|||||||
applicationId = "com.a2i.forwarder"
|
applicationId = "com.a2i.forwarder"
|
||||||
minSdk = 34
|
minSdk = 34
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 37
|
versionCode = 38
|
||||||
versionName = "1.9.8"
|
versionName = "1.9.9"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.a2i.forwarder.core.ConnectivityMonitor
|
|||||||
import com.a2i.forwarder.core.LogStore
|
import com.a2i.forwarder.core.LogStore
|
||||||
import com.a2i.forwarder.core.PhoneCallMonitor
|
import com.a2i.forwarder.core.PhoneCallMonitor
|
||||||
import com.a2i.forwarder.core.SmsFallbackForwarder
|
import com.a2i.forwarder.core.SmsFallbackForwarder
|
||||||
|
import com.a2i.forwarder.core.Telemetry
|
||||||
import com.a2i.forwarder.core.UpdateChecker
|
import com.a2i.forwarder.core.UpdateChecker
|
||||||
import com.a2i.forwarder.store.AppRulesStore
|
import com.a2i.forwarder.store.AppRulesStore
|
||||||
import com.a2i.forwarder.store.SettingsStore
|
import com.a2i.forwarder.store.SettingsStore
|
||||||
@@ -33,6 +34,8 @@ class A2iApp : Application() {
|
|||||||
private set
|
private set
|
||||||
lateinit var updateChecker: UpdateChecker
|
lateinit var updateChecker: UpdateChecker
|
||||||
private set
|
private set
|
||||||
|
lateinit var telemetry: Telemetry
|
||||||
|
private set
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@@ -48,6 +51,8 @@ class A2iApp : Application() {
|
|||||||
smsForwarder = SmsFallbackForwarder(this)
|
smsForwarder = SmsFallbackForwarder(this)
|
||||||
updateChecker = UpdateChecker(this)
|
updateChecker = UpdateChecker(this)
|
||||||
updateChecker.start()
|
updateChecker.start()
|
||||||
|
telemetry = Telemetry(this, appScope)
|
||||||
|
telemetry.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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