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,320 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import AdSupport
|
||||
import AppTrackingTransparency
|
||||
import CFNetwork
|
||||
import CoreTelephony
|
||||
import Darwin
|
||||
|
||||
public class AnalyticsSdkPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
|
||||
private var proximityEventSink: FlutterEventSink?
|
||||
private var proximityMonitoringWasEnabled = false
|
||||
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "analytics_sdk", binaryMessenger: registrar.messenger())
|
||||
let instance = AnalyticsSdkPlugin()
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
let proximityChannel = FlutterEventChannel(
|
||||
name: "analytics_sdk/proximity",
|
||||
binaryMessenger: registrar.messenger()
|
||||
)
|
||||
proximityChannel.setStreamHandler(instance)
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "getPlatformVersion":
|
||||
result("iOS " + UIDevice.current.systemVersion)
|
||||
case "getDeviceId":
|
||||
result(UIDevice.current.identifierForVendor?.uuidString ?? "")
|
||||
case "getSdkInitPlatformFields":
|
||||
result(sdkInitPlatformFields())
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
public func onListen(
|
||||
withArguments arguments: Any?,
|
||||
eventSink events: @escaping FlutterEventSink
|
||||
) -> FlutterError? {
|
||||
_ = onCancel(withArguments: nil)
|
||||
proximityEventSink = events
|
||||
proximityMonitoringWasEnabled = UIDevice.current.isProximityMonitoringEnabled
|
||||
UIDevice.current.isProximityMonitoringEnabled = true
|
||||
guard UIDevice.current.isProximityMonitoringEnabled else {
|
||||
proximityEventSink = nil
|
||||
return FlutterError(
|
||||
code: "proximity_unavailable",
|
||||
message: "Proximity monitoring unavailable",
|
||||
details: nil
|
||||
)
|
||||
}
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(proximityStateDidChange),
|
||||
name: UIDevice.proximityStateDidChangeNotification,
|
||||
object: UIDevice.current
|
||||
)
|
||||
events(UIDevice.current.proximityState ? 1 : 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||||
NotificationCenter.default.removeObserver(
|
||||
self,
|
||||
name: UIDevice.proximityStateDidChangeNotification,
|
||||
object: UIDevice.current
|
||||
)
|
||||
UIDevice.current.isProximityMonitoringEnabled = proximityMonitoringWasEnabled
|
||||
proximityEventSink = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc private func proximityStateDidChange() {
|
||||
proximityEventSink?(UIDevice.current.proximityState ? 1 : 0)
|
||||
}
|
||||
|
||||
private func sdkInitPlatformFields() -> [String: Any] {
|
||||
var fields: [String: Any] = [
|
||||
"language": normalizedLocaleIdentifier(),
|
||||
"timezone": TimeZone.current.identifier,
|
||||
"bundle_id": Bundle.main.bundleIdentifier ?? "",
|
||||
"ios_device_model_raw": rawDeviceModel(),
|
||||
"idfv": UIDevice.current.identifierForVendor?.uuidString ?? "",
|
||||
"is_jailbroken": isJailbroken() ? 1 : 0,
|
||||
"cpu_cores": ProcessInfo.processInfo.activeProcessorCount,
|
||||
"device_memory_gb": bytesToGb(Int64(ProcessInfo.processInfo.physicalMemory)),
|
||||
"touch_support": 1
|
||||
]
|
||||
|
||||
fields.merge(screenFields()) { _, new in new }
|
||||
fields.merge(storageFields()) { _, new in new }
|
||||
fields.merge(batteryFields()) { _, new in new }
|
||||
fields.merge(telephonyFields()) { _, new in new }
|
||||
fields.merge(advertisingFields()) { _, new in new }
|
||||
fields.merge(networkStateFields()) { _, new in new }
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
private func normalizedLocaleIdentifier() -> String {
|
||||
return Locale.current.identifier.replacingOccurrences(of: "_", with: "-")
|
||||
}
|
||||
|
||||
private func rawDeviceModel() -> String {
|
||||
var systemInfo = utsname()
|
||||
uname(&systemInfo)
|
||||
let mirror = Mirror(reflecting: systemInfo.machine)
|
||||
let identifier = mirror.children.reduce(into: "") { value, element in
|
||||
guard let byte = element.value as? Int8, byte != 0 else { return }
|
||||
value.append(String(UnicodeScalar(UInt8(byte))))
|
||||
}
|
||||
return identifier
|
||||
}
|
||||
|
||||
private func screenFields() -> [String: Any] {
|
||||
let screen = UIScreen.main
|
||||
let nativeBounds = screen.nativeBounds
|
||||
let scale = screen.scale
|
||||
return [
|
||||
"screen_width": Int(nativeBounds.width.rounded()),
|
||||
"screen_height": Int(nativeBounds.height.rounded()),
|
||||
"screen_dpi": Int((scale * 160).rounded()),
|
||||
"device_pixel_ratio": scale,
|
||||
"orientation": interfaceOrientation()
|
||||
]
|
||||
}
|
||||
|
||||
private func interfaceOrientation() -> String {
|
||||
if #available(iOS 13.0, *) {
|
||||
let orientation = UIApplication.shared.connectedScenes
|
||||
.compactMap { $0 as? UIWindowScene }
|
||||
.first?
|
||||
.interfaceOrientation
|
||||
if orientation?.isLandscape == true {
|
||||
return "landscape"
|
||||
}
|
||||
if orientation?.isPortrait == true {
|
||||
return "portrait"
|
||||
}
|
||||
} else {
|
||||
let orientation = UIApplication.shared.statusBarOrientation
|
||||
if orientation.isLandscape {
|
||||
return "landscape"
|
||||
}
|
||||
if orientation.isPortrait {
|
||||
return "portrait"
|
||||
}
|
||||
}
|
||||
return screenFallbackOrientation()
|
||||
}
|
||||
|
||||
private func screenFallbackOrientation() -> String {
|
||||
let bounds = UIScreen.main.nativeBounds
|
||||
return bounds.width >= bounds.height ? "landscape" : "portrait"
|
||||
}
|
||||
|
||||
private func storageFields() -> [String: Any] {
|
||||
do {
|
||||
let attributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory())
|
||||
let total = (attributes[.systemSize] as? NSNumber)?.int64Value ?? -1
|
||||
let free = (attributes[.systemFreeSize] as? NSNumber)?.int64Value ?? -1
|
||||
return [
|
||||
"storage": bytesToGb(total),
|
||||
"available_storage": bytesToGb(free)
|
||||
]
|
||||
} catch {
|
||||
return ["storage": -1, "available_storage": -1]
|
||||
}
|
||||
}
|
||||
|
||||
private func batteryFields() -> [String: Any] {
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
let level = UIDevice.current.batteryLevel
|
||||
let batteryLevel = level >= 0 ? Int((level * 100).rounded()) : -1
|
||||
let charging: Int
|
||||
switch UIDevice.current.batteryState {
|
||||
case .charging, .full:
|
||||
charging = 1
|
||||
case .unplugged:
|
||||
charging = 0
|
||||
default:
|
||||
charging = -1
|
||||
}
|
||||
return ["battery_level": batteryLevel, "is_charging": charging]
|
||||
}
|
||||
|
||||
private func telephonyFields() -> [String: Any] {
|
||||
var fields: [String: Any] = [
|
||||
"carrier": "",
|
||||
"mcc": "unknown",
|
||||
"mnc": "unknown",
|
||||
"has_sim": -1
|
||||
]
|
||||
|
||||
let info = CTTelephonyNetworkInfo()
|
||||
var carrier: CTCarrier?
|
||||
if #available(iOS 12.0, *) {
|
||||
carrier = info.serviceSubscriberCellularProviders?.values.first
|
||||
} else {
|
||||
carrier = info.subscriberCellularProvider
|
||||
}
|
||||
|
||||
guard let carrier = carrier else {
|
||||
fields["has_sim"] = 0
|
||||
return fields
|
||||
}
|
||||
|
||||
fields["carrier"] = carrier.carrierName ?? ""
|
||||
fields["mcc"] = carrier.mobileCountryCode ?? "unknown"
|
||||
fields["mnc"] = carrier.mobileNetworkCode ?? "unknown"
|
||||
fields["has_sim"] = 1
|
||||
return fields
|
||||
}
|
||||
|
||||
private func advertisingFields() -> [String: Any] {
|
||||
if #available(iOS 14, *) {
|
||||
switch ATTrackingManager.trackingAuthorizationStatus {
|
||||
case .authorized:
|
||||
return [
|
||||
"idfa": ASIdentifierManager.shared().advertisingIdentifier.uuidString,
|
||||
"att_status": "authorized",
|
||||
"lat": 0
|
||||
]
|
||||
case .denied:
|
||||
return ["idfa": "", "att_status": "denied", "lat": 1]
|
||||
case .restricted:
|
||||
return ["idfa": "", "att_status": "restricted", "lat": 1]
|
||||
case .notDetermined:
|
||||
return ["idfa": "", "att_status": "not_determined", "lat": -1]
|
||||
@unknown default:
|
||||
return ["idfa": "", "att_status": "unknown", "lat": -1]
|
||||
}
|
||||
} else {
|
||||
let manager = ASIdentifierManager.shared()
|
||||
let trackingEnabled = manager.isAdvertisingTrackingEnabled
|
||||
return [
|
||||
"idfa": trackingEnabled ? manager.advertisingIdentifier.uuidString : "",
|
||||
"att_status": "unknown",
|
||||
"lat": trackingEnabled ? 0 : 1
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private func networkStateFields() -> [String: Any] {
|
||||
return [
|
||||
"vpn_enabled": isVpnEnabled() ? 1 : 0,
|
||||
"proxy_enabled": isProxyEnabled()
|
||||
]
|
||||
}
|
||||
|
||||
private func isProxyEnabled() -> Int {
|
||||
guard
|
||||
let settings = CFNetworkCopySystemProxySettings()?.takeRetainedValue() as? [String: Any]
|
||||
else {
|
||||
return -1
|
||||
}
|
||||
|
||||
let httpEnabled = (settings[kCFNetworkProxiesHTTPEnable as String] as? NSNumber)?.intValue == 1
|
||||
let httpsEnabled = (settings["HTTPSEnable"] as? NSNumber)?.intValue == 1
|
||||
return httpEnabled || httpsEnabled ? 1 : 0
|
||||
}
|
||||
|
||||
private func isVpnEnabled() -> Bool {
|
||||
var addresses: UnsafeMutablePointer<ifaddrs>?
|
||||
guard getifaddrs(&addresses) == 0, let firstAddress = addresses else {
|
||||
return false
|
||||
}
|
||||
defer { freeifaddrs(firstAddress) }
|
||||
|
||||
var pointer: UnsafeMutablePointer<ifaddrs>? = firstAddress
|
||||
while pointer != nil {
|
||||
if let name = pointer?.pointee.ifa_name {
|
||||
let interface = String(cString: name)
|
||||
if interface.hasPrefix("utun") ||
|
||||
interface.hasPrefix("ppp") ||
|
||||
interface.hasPrefix("ipsec") ||
|
||||
interface.hasPrefix("tap") ||
|
||||
interface.hasPrefix("tun") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
pointer = pointer?.pointee.ifa_next
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func isJailbroken() -> Bool {
|
||||
#if targetEnvironment(simulator)
|
||||
return false
|
||||
#else
|
||||
let paths = [
|
||||
"/Applications/Cydia.app",
|
||||
"/Library/MobileSubstrate/MobileSubstrate.dylib",
|
||||
"/bin/bash",
|
||||
"/usr/sbin/sshd",
|
||||
"/etc/apt",
|
||||
"/private/var/lib/apt/"
|
||||
]
|
||||
if paths.contains(where: { FileManager.default.fileExists(atPath: $0) }) {
|
||||
return true
|
||||
}
|
||||
do {
|
||||
let path = "/private/analytics_sdk_jailbreak_test.txt"
|
||||
try "test".write(toFile: path, atomically: true, encoding: .utf8)
|
||||
try FileManager.default.removeItem(atPath: path)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func bytesToGb(_ bytes: Int64) -> Int64 {
|
||||
if bytes <= 0 {
|
||||
return -1
|
||||
}
|
||||
return bytes / (1024 * 1024 * 1024)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user