Initial commit: 91dj android client dev_1.0.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-10-08 11:37:25 +07:00
co-authored by Claude Opus 5.5
commit 97dd6a739a
1262 changed files with 119813 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx
+64
View File
@@ -0,0 +1,64 @@
group = "com.example.analytics_sdk"
version = "1.0-SNAPSHOT"
// Flutter plugins are built within the host application's Gradle process.
// Do not pin AGP or Kotlin versions here: Flutter 3.0 hosts still use
// Gradle 6 / AGP 4, while newer hosts use AGP 8.
rootProject.allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
maven { url 'https://developer.huawei.com/repo/' }
maven { url 'https://developer.honor.com/repo/' }
}
}
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
android {
if (project.android.hasProperty("namespace")) {
namespace = "com.example.analytics_sdk"
}
// AGP 4.x understands compileSdkVersion; AGP 7/8 retains it as a
// compatible (deprecated) alias.
compileSdkVersion 34
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
sourceSets {
main.java.srcDirs += "src/main/kotlin"
test.java.srcDirs += "src/test/kotlin"
}
defaultConfig {
minSdkVersion 21
}
testOptions {
unitTests.all {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}
dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.0.0")
implementation 'com.github.gzu-liyujiang:Android_CN_OAID:4.2.16'
}
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+1
View File
@@ -0,0 +1 @@
rootProject.name = 'analytics_sdk'
@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.analytics_sdk">
</manifest>
@@ -0,0 +1,491 @@
package com.example.analytics_sdk
import android.app.ActivityManager
import android.app.Application
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.StatFs
import android.provider.Settings
import android.telephony.TelephonyManager
import android.util.Log
import com.github.gzuliyujiang.oaid.DeviceID
import com.github.gzuliyujiang.oaid.DeviceIdentifier
import com.github.gzuliyujiang.oaid.IGetter
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel
import java.io.File
import java.util.TimeZone
class AnalyticsSdkPlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler {
private lateinit var channel : MethodChannel
private var proximityChannel: EventChannel? = null
private lateinit var context: Context
private val mainHandler = Handler(Looper.getMainLooper())
private var proximityEventSink: EventChannel.EventSink? = null
private var proximityListener: SensorEventListener? = null
private companion object {
private const val LOG_TAG = "AnalyticsSdk"
private const val LOG_CHUNK_SIZE = 1000
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
registerDeviceIdentifier()
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "analytics_sdk")
channel.setMethodCallHandler(this)
proximityChannel = EventChannel(
flutterPluginBinding.binaryMessenger,
"analytics_sdk/proximity"
)
proximityChannel?.setStreamHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"getPlatformVersion" -> result.success("Android ${Build.VERSION.RELEASE}")
"getDeviceId" -> result.success(deviceId())
"getSdkInitPlatformFields" -> collectSdkInitPlatformFields(result)
else -> result.notImplemented()
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
stopProximityEvents()
proximityChannel?.setStreamHandler(null)
proximityChannel = null
channel.setMethodCallHandler(null)
}
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
stopProximityEvents()
proximityEventSink = events
startProximityEvents(events)
}
override fun onCancel(arguments: Any?) {
stopProximityEvents()
}
private fun collectSdkInitPlatformFields(result: Result) {
collectAdvertisingIdFields { advertisingIdFields ->
result.success(getSdkInitPlatformFields(advertisingIdFields))
}
}
private fun getSdkInitPlatformFields(
advertisingIdFields: Map<String, Any>
): Map<String, Any> {
val fields = mutableMapOf<String, Any>()
fields["package_name"] = context.packageName
fields["timezone"] = TimeZone.getDefault().id
fields["brand"] = unknownIfBlank(Build.BRAND)
fields["manufacturer"] = unknownIfBlank(Build.MANUFACTURER)
fields["model"] = unknownIfBlank(Build.MODEL)
fields["abi"] = unknownIfBlank(Build.SUPPORTED_ABIS.firstOrNull())
fields["build_hardware"] = unknownIfBlank(Build.HARDWARE)
fields["build_product"] = unknownIfBlank(Build.PRODUCT)
fields["build_fingerprint"] = unknownIfBlank(Build.FINGERPRINT)
fields.putAll(telephonyFields())
fields.putAll(hardwareCapabilityFields())
fields.putAll(storageFields())
fields.putAll(networkStateFields())
fields.putAll(androidIdFields())
fields.putAll(advertisingIdFields)
fields.putAll(debugStateFields())
fields.putAll(sensorFields())
fields["install_source"] = installSource()
fields["is_rooted"] = rootedState()
fields["has_baseband"] = hasBaseband()
return fields
}
private fun hardwareCapabilityFields(): Map<String, Any> {
val fields = mutableMapOf<String, Any>(
"cpu_cores" to -1,
"device_memory_gb" to -1,
"touch_support" to -1,
"max_touch_points" to -1
)
try {
fields["cpu_cores"] = Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
} catch (_: Throwable) {
}
try {
val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
if (manager != null) {
val memoryInfo = ActivityManager.MemoryInfo()
manager.getMemoryInfo(memoryInfo)
fields["device_memory_gb"] = bytesToGb(memoryInfo.totalMem)
}
} catch (_: Throwable) {
}
try {
val packageManager = context.packageManager
val hasTouch = packageManager.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)
fields["touch_support"] = if (hasTouch) 1 else 0
fields["max_touch_points"] = when {
packageManager.hasSystemFeature(
PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND
) -> 5
packageManager.hasSystemFeature(
PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT
) -> 2
packageManager.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH) -> 2
hasTouch -> 1
else -> 0
}
} catch (_: Throwable) {
}
return fields
}
private fun telephonyFields(): Map<String, Any> {
val fields = mutableMapOf<String, Any>(
"carrier" to "",
"mcc" to "unknown",
"mnc" to "unknown",
"has_sim" to -1
)
try {
val manager = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
?: return fields
fields["carrier"] = manager.simOperatorName ?: ""
val operator = manager.simOperator ?: ""
if (operator.length >= 3) fields["mcc"] = operator.substring(0, 3)
if (operator.length > 3) fields["mnc"] = operator.substring(3)
fields["has_sim"] = when (manager.simState) {
TelephonyManager.SIM_STATE_READY -> 1
TelephonyManager.SIM_STATE_ABSENT -> 0
else -> -1
}
} catch (_: Throwable) {
}
return fields
}
private fun storageFields(): Map<String, Any> {
return try {
val stat = StatFs(context.applicationInfo.dataDir)
mapOf(
"storage" to bytesToGb(stat.totalBytes),
"available_storage" to bytesToGb(stat.availableBytes)
)
} catch (_: Throwable) {
mapOf("storage" to -1, "available_storage" to -1)
}
}
private fun networkStateFields(): Map<String, Any> {
val fields = mutableMapOf<String, Any>(
"vpn_enabled" to -1,
"proxy_enabled" to -1
)
try {
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
val capabilities = manager?.getNetworkCapabilities(manager.activeNetwork)
if (capabilities == null) return fields
fields["vpn_enabled"] =
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) 1 else 0
} catch (_: Throwable) {
}
try {
val proxyHost = System.getProperty("http.proxyHost")?.trim().orEmpty()
fields["proxy_enabled"] = if (proxyHost.isNotEmpty()) 1 else 0
} catch (_: Throwable) {
}
return fields
}
private fun androidIdFields(): Map<String, Any> {
return try {
val androidId = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
)?.trim().orEmpty()
if (androidId.isNotEmpty()) {
mapOf(
"android_id" to hashIdentifier(androidId),
"android_id_o" to androidId,
"android_id_status" to "available"
)
} else {
mapOf(
"android_id" to "",
"android_id_o" to "",
"android_id_status" to "unavailable"
)
}
} catch (_: Throwable) {
mapOf(
"android_id" to "",
"android_id_o" to "",
"android_id_status" to "unknown"
)
}
}
private fun collectAdvertisingIdFields(onReady: (Map<String, Any>) -> Unit) {
val clientIdValue = resolveClientIdRaw()
val rawGuid = guid()
val fields = mutableMapOf<String, Any>(
"oaid" to hashIdentifier(clientIdValue),
"oaid_o" to clientIdValue,
"aaid" to "",
"aaid_o" to "",
"guid" to hashIdentifier(rawGuid),
"guid_o" to rawGuid
)
val lock = Any()
var completed = false
fun finishAaid(rawValue: String?) {
val aaidValue = rawValue?.trim().orEmpty()
val snapshot = synchronized(lock) {
if (completed) {
null
} else {
completed = true
fields["aaid"] = hashIdentifier(aaidValue)
fields["aaid_o"] = aaidValue
fields.toMap()
}
}
snapshot?.let { mainHandler.post { onReady(it) } }
}
mainHandler.postDelayed({
finishAaid("")
}, 1500L)
try {
DeviceID.getByGms(context, object : IGetter {
override fun onOAIDGetComplete(result: String?) {
finishAaid(result)
}
override fun onOAIDGetError(error: Exception?) {
finishAaid("")
}
})
} catch (_: Throwable) {
finishAaid("")
}
}
private fun guid(): String {
return try {
DeviceIdentifier.getGUID(context)?.trim().orEmpty()
} catch (_: Throwable) {
""
}
}
@Suppress("DEPRECATION")
private fun resolveClientIdRaw(): String {
return ClientIdentifierResolver.resolve(
{ DeviceIdentifier.getIMEI(context) },
{ DeviceIdentifier.getOAID(context) },
{ DeviceIdentifier.getAndroidID(context) },
{ DeviceIdentifier.getGUID(context) }
)
}
private fun deviceId(): String {
return hashIdentifier(resolveClientIdRaw())
}
private fun hashIdentifier(value: String?): String {
val text = value?.trim().orEmpty()
if (text.isEmpty()) return ""
return DeviceID.calculateHash(text, "MD5")
}
private fun registerDeviceIdentifier() {
val application = context.applicationContext as? Application
?: return
try {
DeviceIdentifier.register(application)
} catch (_: Throwable) {}
}
private fun debugStateFields(): Map<String, Any> {
val fields = mutableMapOf<String, Any>(
"developer_options_enabled" to -1,
"adb_enabled" to -1,
"usb_debug_enabled" to -1
)
try {
val developerOptions = Settings.Global.getInt(
context.contentResolver,
Settings.Global.DEVELOPMENT_SETTINGS_ENABLED,
0
)
fields["developer_options_enabled"] = if (developerOptions == 1) 1 else 0
} catch (_: Throwable) {
}
try {
val adb = Settings.Global.getInt(
context.contentResolver,
Settings.Global.ADB_ENABLED,
0
)
val enabled = if (adb == 1) 1 else 0
fields["adb_enabled"] = enabled
fields["usb_debug_enabled"] = enabled
} catch (_: Throwable) {
}
return fields
}
private fun sensorFields(): Map<String, Any> {
val fields = mutableMapOf<String, Any>(
"sensor_count_supported" to -1,
"has_accelerometer" to -1,
"has_gyroscope" to -1,
"has_magnetometer" to -1,
"has_light_sensor" to -1,
"has_proximity_sensor" to -1
)
try {
val manager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
?: return fields
fields["sensor_count_supported"] = manager.getSensorList(Sensor.TYPE_ALL).size
fields["has_accelerometer"] =
if (manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null) 1 else 0
fields["has_gyroscope"] =
if (manager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null) 1 else 0
fields["has_magnetometer"] =
if (manager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) != null) 1 else 0
fields["has_light_sensor"] =
if (manager.getDefaultSensor(Sensor.TYPE_LIGHT) != null) 1 else 0
fields["has_proximity_sensor"] =
if (manager.getDefaultSensor(Sensor.TYPE_PROXIMITY) != null) 1 else 0
} catch (_: Throwable) {
}
return fields
}
private fun startProximityEvents(events: EventChannel.EventSink?) {
try {
val manager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
if (manager == null) {
events?.error("proximity_unavailable", "SensorManager unavailable", null)
return
}
val sensor = manager.getDefaultSensor(Sensor.TYPE_PROXIMITY)
if (sensor == null) {
events?.error("proximity_unavailable", "Proximity sensor unavailable", null)
return
}
val listener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
val value = event.values.firstOrNull() ?: return
val isNear = value < sensor.maximumRange
mainHandler.post {
proximityEventSink?.success(if (isNear) 1 else 0)
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
}
}
proximityListener = listener
manager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
} catch (e: Throwable) {
events?.error("proximity_error", e.message ?: "Proximity listener failed", null)
}
}
private fun stopProximityEvents() {
try {
val manager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
val listener = proximityListener
if (manager != null && listener != null) {
manager.unregisterListener(listener)
}
} catch (_: Throwable) {
}
proximityListener = null
proximityEventSink = null
}
private fun installSource(): String {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
context.packageManager.getInstallSourceInfo(context.packageName)
.installingPackageName ?: ""
} else {
@Suppress("DEPRECATION")
context.packageManager.getInstallerPackageName(context.packageName) ?: ""
}
} catch (_: Throwable) {
""
}
}
private fun hasBaseband(): Int {
return try {
val radio = Build.getRadioVersion()?.trim().orEmpty()
if (radio.isEmpty() || radio.equals("unknown", ignoreCase = true)) -1 else 1
} catch (_: Throwable) {
-1
}
}
private fun rootedState(): Int {
return try {
if (isRooted()) 1 else 0
} catch (_: Throwable) {
-1
}
}
private fun isRooted(): Boolean {
val tags = Build.TAGS ?: ""
if (tags.contains("test-keys")) return true
val paths = listOf(
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su"
)
return paths.any { File(it).exists() }
}
private fun bytesToGb(bytes: Long): Long {
if (bytes <= 0L) return -1L
return bytes / (1024L * 1024L * 1024L)
}
private fun unknownIfBlank(value: String?): String {
val text = value?.trim().orEmpty()
return if (text.isEmpty()) "unknown" else text
}
}
@@ -0,0 +1,27 @@
package com.example.analytics_sdk
import java.util.Locale
internal object ClientIdentifierResolver {
fun resolve(vararg candidates: () -> String?): String {
for (candidate in candidates) {
val value = try {
candidate()?.trim().orEmpty()
} catch (_: Throwable) {
continue
}
if (isValid(value)) return value
}
return ""
}
private fun isValid(value: String?): Boolean {
val text = value?.trim().orEmpty()
if (text.isEmpty()) return false
when (text.lowercase(Locale.ROOT)) {
"null", "unknown", "unkown" -> return false
}
val withoutHyphens = text.replace("-", "")
return withoutHyphens.isNotEmpty() && !withoutHyphens.all { it == '0' }
}
}
@@ -0,0 +1,80 @@
package com.example.analytics_sdk
import kotlin.test.Test
import kotlin.test.assertEquals
internal class ClientIdentifierResolverTest {
@Test
fun resolve_returnsFirstValidCandidateAndStopsReading() {
val calls = mutableListOf<String>()
val result = ClientIdentifierResolver.resolve(
{ calls += "imei"; " imei-123 " },
{ calls += "oaid"; "oaid-456" },
{ calls += "android"; "android-789" },
{ calls += "guid"; "guid-000" }
)
assertEquals("imei-123", result)
assertEquals(listOf("imei"), calls)
}
@Test
fun resolve_skipsEverySupportedInvalidSentinel() {
val invalidValues = listOf<String?>(
null,
"",
" ",
"null",
"NULL",
"unknown",
"UnKnOwN",
"unkown",
"0",
"000000",
"---",
"000000-000-0000-00000000",
"00000000-0000-0000-0000-000000000000"
)
for (invalidValue in invalidValues) {
val result = ClientIdentifierResolver.resolve(
{ invalidValue },
{ "android-id" }
)
assertEquals("android-id", result, "invalidValue=$invalidValue")
}
}
@Test
fun resolve_preservesValidHyphenatedIdentifier() {
val result = ClientIdentifierResolver.resolve(
{ "00000000-0000-0000-0000-00000000000a" },
{ "fallback" }
)
assertEquals("00000000-0000-0000-0000-00000000000a", result)
}
@Test
fun resolve_skipsThrowingCandidate() {
val result = ClientIdentifierResolver.resolve(
{ throw IllegalStateException("imei unavailable") },
{ "oaid-123" }
)
assertEquals("oaid-123", result)
}
@Test
fun resolve_returnsEmptyWhenEveryCandidateIsInvalid() {
val result = ClientIdentifierResolver.resolve(
{ null },
{ "unknown" },
{ "0000-0000" },
{ "" }
)
assertEquals("", result)
}
}
@@ -0,0 +1,27 @@
package com.example.analytics_sdk
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlin.test.Test
import org.mockito.Mockito
/*
* This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
*
* Once you have built the plugin's example app, you can run these tests from the command
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
* you can run them directly from IDEs that support JUnit such as Android Studio.
*/
internal class AnalyticsSdkPluginTest {
@Test
fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
val plugin = AnalyticsSdkPlugin()
val call = MethodCall("getPlatformVersion", null)
val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
plugin.onMethodCall(call, mockResult)
Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE)
}
}