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? guard getifaddrs(&addresses) == 0, let firstAddress = addresses else { return false } defer { freeifaddrs(firstAddress) } var pointer: UnsafeMutablePointer? = 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) } }