Initial commit: 91dj android client dev_1.0.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.cxx
|
||||
@@ -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
|
||||
@@ -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>
|
||||
+491
@@ -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
|
||||
}
|
||||
}
|
||||
+27
@@ -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' }
|
||||
}
|
||||
}
|
||||
+80
@@ -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)
|
||||
}
|
||||
}
|
||||
+27
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user