Files
51dj_android/analytics_sdk/ios/Classes/DataPlusSdkPlugin.swift
T
2026-10-08 11:37:25 +07:00

321 lines
9.9 KiB
Swift

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)
}
}