From 98891eb90485fe97b8e699c4b61368ec90f64e33 Mon Sep 17 00:00:00 2001 From: twodayslate Date: Mon, 27 Jun 2022 21:47:01 -0400 Subject: [PATCH] Convert Device tab to SwiftUI (#96) --- ec3730.xcodeproj/project.pbxproj | 72 +++++++++++++++ ec3730/Controllers/SRCTabBarController.swift | 7 +- ec3730/Models/Device/DeviceInfoModel.swift | 56 +++++++++++ .../Device/DeviceInfoSectionModel.swift | 9 ++ .../Device/Sections/CarrierInfoModel.swift | 74 +++++++++++++++ .../Sections/FingerprintInfoModel.swift | 32 +++++++ .../Device/Sections/JavaScriptInfoModel.swift | 79 ++++++++++++++++ .../Device/Sections/MemoryInfoModel.swift | 91 ++++++++++++++++++ .../Device/Sections/ProcessInfoModel.swift | 59 ++++++++++++ .../Device/Sections/UIDeviceInfoModel.swift | 58 ++++++++++++ ec3730/Models/FingerprintModel.swift | 37 ++++++++ .../Views/Device/DeviceInfoSectionView.swift | 53 +++++++++++ ec3730/Views/Device/DeviceInfoView.swift | 92 +++++++++++++++++++ ec3730/Views/WebkitOverlayView.swift | 37 ++++++++ 14 files changed, 755 insertions(+), 1 deletion(-) create mode 100644 ec3730/Models/Device/DeviceInfoModel.swift create mode 100644 ec3730/Models/Device/DeviceInfoSectionModel.swift create mode 100644 ec3730/Models/Device/Sections/CarrierInfoModel.swift create mode 100644 ec3730/Models/Device/Sections/FingerprintInfoModel.swift create mode 100644 ec3730/Models/Device/Sections/JavaScriptInfoModel.swift create mode 100644 ec3730/Models/Device/Sections/MemoryInfoModel.swift create mode 100644 ec3730/Models/Device/Sections/ProcessInfoModel.swift create mode 100644 ec3730/Models/Device/Sections/UIDeviceInfoModel.swift create mode 100644 ec3730/Models/FingerprintModel.swift create mode 100644 ec3730/Views/Device/DeviceInfoSectionView.swift create mode 100644 ec3730/Views/Device/DeviceInfoView.swift create mode 100644 ec3730/Views/WebkitOverlayView.swift diff --git a/ec3730.xcodeproj/project.pbxproj b/ec3730.xcodeproj/project.pbxproj index db58d8e..0ccb161 100644 --- a/ec3730.xcodeproj/project.pbxproj +++ b/ec3730.xcodeproj/project.pbxproj @@ -53,6 +53,18 @@ F650F74B233E62C100BAA609 /* BlurredPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F650F74A233E62C100BAA609 /* BlurredPickerView.swift */; }; F65FDA6527AB03BC0024521B /* WhoisXmlReputationSectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F65FDA6427AB03BC0024521B /* WhoisXmlReputationSectionModel.swift */; }; F65FDA6727AB06920024521B /* WhoisXmlReputationRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = F65FDA6627AB06920024521B /* WhoisXmlReputationRecord.swift */; }; + F660FDD52868E9550027C759 /* DeviceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F660FDD42868E9550027C759 /* DeviceInfoView.swift */; }; + F660FDD82868E9A20027C759 /* DeviceInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F660FDD72868E9A20027C759 /* DeviceInfoModel.swift */; }; + F660FDDB2868EA8C0027C759 /* DeviceInfoSectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F660FDDA2868EA8C0027C759 /* DeviceInfoSectionModel.swift */; }; + F660FDDE2868EAAC0027C759 /* CarrierInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F660FDDD2868EAAC0027C759 /* CarrierInfoModel.swift */; }; + F660FDE02868F3310027C759 /* UIDeviceInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F660FDDF2868F3310027C759 /* UIDeviceInfoModel.swift */; }; + F660FDE2286900000027C759 /* ProcessInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F660FDE1286900000027C759 /* ProcessInfoModel.swift */; }; + F660FDE6286902AF0027C759 /* MemoryInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F660FDE5286902AF0027C759 /* MemoryInfoModel.swift */; }; + F660FDE8286903DC0027C759 /* JavaScriptInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F660FDE7286903DC0027C759 /* JavaScriptInfoModel.swift */; }; + F660FDEA28692CFB0027C759 /* WebkitOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F660FDE928692CFB0027C759 /* WebkitOverlayView.swift */; }; + F660FDEC28692EB10027C759 /* FingerprintModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F660FDEB28692EB10027C759 /* FingerprintModel.swift */; }; + F660FDEE28693D020027C759 /* FingerprintInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F660FDED28693D010027C759 /* FingerprintInfoModel.swift */; }; + F660FDF0286953A00027C759 /* DeviceInfoSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F660FDEF286953A00027C759 /* DeviceInfoSectionView.swift */; }; F661A28023020FCF0023FA50 /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F661A27F23020FCF0023FA50 /* UIAlertController.swift */; }; F66643B326F02A5B000F17BC /* HostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F66643B226F02A5B000F17BC /* HostViewModel.swift */; }; F66643B526F02A8F000F17BC /* HostViewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F66643B426F02A8F000F17BC /* HostViewSection.swift */; }; @@ -190,6 +202,18 @@ F650F74A233E62C100BAA609 /* BlurredPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurredPickerView.swift; sourceTree = ""; }; F65FDA6427AB03BC0024521B /* WhoisXmlReputationSectionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhoisXmlReputationSectionModel.swift; sourceTree = ""; }; F65FDA6627AB06920024521B /* WhoisXmlReputationRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhoisXmlReputationRecord.swift; sourceTree = ""; }; + F660FDD42868E9550027C759 /* DeviceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoView.swift; sourceTree = ""; }; + F660FDD72868E9A20027C759 /* DeviceInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoModel.swift; sourceTree = ""; }; + F660FDDA2868EA8C0027C759 /* DeviceInfoSectionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoSectionModel.swift; sourceTree = ""; }; + F660FDDD2868EAAC0027C759 /* CarrierInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarrierInfoModel.swift; sourceTree = ""; }; + F660FDDF2868F3310027C759 /* UIDeviceInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDeviceInfoModel.swift; sourceTree = ""; }; + F660FDE1286900000027C759 /* ProcessInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfoModel.swift; sourceTree = ""; }; + F660FDE5286902AF0027C759 /* MemoryInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryInfoModel.swift; sourceTree = ""; }; + F660FDE7286903DC0027C759 /* JavaScriptInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JavaScriptInfoModel.swift; sourceTree = ""; }; + F660FDE928692CFB0027C759 /* WebkitOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebkitOverlayView.swift; sourceTree = ""; }; + F660FDEB28692EB10027C759 /* FingerprintModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FingerprintModel.swift; sourceTree = ""; }; + F660FDED28693D010027C759 /* FingerprintInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FingerprintInfoModel.swift; sourceTree = ""; }; + F660FDEF286953A00027C759 /* DeviceInfoSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoSectionView.swift; sourceTree = ""; }; F661A27F23020FCF0023FA50 /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = ""; }; F66643B226F02A5B000F17BC /* HostViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostViewModel.swift; sourceTree = ""; }; F66643B426F02A8F000F17BC /* HostViewSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostViewSection.swift; sourceTree = ""; }; @@ -337,6 +361,7 @@ isa = PBXGroup; children = ( F6397FE425E8937900914B3F /* HostView.swift */, + F660FDD62868E9590027C759 /* Device */, F63B9227279BC31B007E845F /* HostHistoryList.swift */, F63B922B279C9079007E845F /* HostResult.swift */, F6A6410B2844235D00E00F7F /* HostBarView.swift */, @@ -344,6 +369,7 @@ F644A0F9279268BD0061C98B /* FSDisclosureGroup.swift */, F644A0F727925CF60061C98B /* HostModelWrapperView.swift */, F66643B426F02A8F000F17BC /* HostViewSection.swift */, + F660FDE928692CFB0027C759 /* WebkitOverlayView.swift */, AA8FBAB8283AA6A000280377 /* HostViewSectionContent.swift */, F6A6410D2844246100E00F7F /* HostViewSectionFocusView.swift */, F6397FEC25EEFEAE00914B3F /* CopyCellView.swift */, @@ -374,10 +400,44 @@ path = Sections; sourceTree = ""; }; + F660FDD62868E9590027C759 /* Device */ = { + isa = PBXGroup; + children = ( + F660FDD42868E9550027C759 /* DeviceInfoView.swift */, + F660FDEF286953A00027C759 /* DeviceInfoSectionView.swift */, + ); + path = Device; + sourceTree = ""; + }; + F660FDD92868EA810027C759 /* Device */ = { + isa = PBXGroup; + children = ( + F660FDD72868E9A20027C759 /* DeviceInfoModel.swift */, + F660FDDA2868EA8C0027C759 /* DeviceInfoSectionModel.swift */, + F660FDDC2868EA900027C759 /* Sections */, + ); + path = Device; + sourceTree = ""; + }; + F660FDDC2868EA900027C759 /* Sections */ = { + isa = PBXGroup; + children = ( + F660FDDD2868EAAC0027C759 /* CarrierInfoModel.swift */, + F660FDE5286902AF0027C759 /* MemoryInfoModel.swift */, + F660FDDF2868F3310027C759 /* UIDeviceInfoModel.swift */, + F660FDE7286903DC0027C759 /* JavaScriptInfoModel.swift */, + F660FDE1286900000027C759 /* ProcessInfoModel.swift */, + F660FDED28693D010027C759 /* FingerprintInfoModel.swift */, + ); + path = Sections; + sourceTree = ""; + }; F66643B126F02A4C000F17BC /* Models */ = { isa = PBXGroup; children = ( F66643B226F02A5B000F17BC /* HostViewModel.swift */, + F660FDEB28692EB10027C759 /* FingerprintModel.swift */, + F660FDD92868EA810027C759 /* Device */, F6C38E8726FABEFB00BD9A71 /* StoreKitModel.swift */, F66643B626F030D5000F17BC /* HostSectionModel.swift */, F644A0FB279283220061C98B /* Sections */, @@ -803,9 +863,11 @@ F6FDDFD8230B106200FBF75E /* WhoisXmlDnsCells.swift in Sources */, F60FD36A25E060D700BCEF86 /* PingItem.swift in Sources */, F68CE56623031DA20068D104 /* DataFeedErrors.swift in Sources */, + F660FDD82868E9A20027C759 /* DeviceInfoModel.swift in Sources */, F69E365423580A7D0017296E /* DataFeedSubscription.swift in Sources */, F69E3652235808DF0017296E /* DFOneTimePurchase.swift in Sources */, F63540D9213DFE8300605ABE /* ApiKeys.swift in Sources */, + F660FDE8286903DC0027C759 /* JavaScriptInfoModel.swift in Sources */, F68CE56A230463A50068D104 /* InterfaceTableViewController.swift in Sources */, F644A0FD279283300061C98B /* LocalDnsModel.swift in Sources */, F6A6410C2844235D00E00F7F /* HostBarView.swift in Sources */, @@ -816,13 +878,17 @@ F63102752130C99D00F34E42 /* PingViewController.swift in Sources */, F6C38E8826FABEFB00BD9A71 /* StoreKitModel.swift in Sources */, F66643B726F030D5000F17BC /* HostSectionModel.swift in Sources */, + F660FDE02868F3310027C759 /* UIDeviceInfoModel.swift in Sources */, F668EB472356616900EEB379 /* DataFeedUserApiKeyTableViewController.swift in Sources */, F619760922FCA39F00CE39A0 /* WhoisLockedTableViewCell.swift in Sources */, F69E3648235805180017296E /* GoogleWebRisk.swift in Sources */, F6C87452234FE8030069A57C /* DataFeedsTableViewController.swift in Sources */, F6FDDFCA23048DEF00FBF75E /* UIView.swift in Sources */, F6FDDFC623047EE300FBF75E /* DefaultsSwitch.swift in Sources */, + F660FDD52868E9550027C759 /* DeviceInfoView.swift in Sources */, F6C591BA235EC3A9009BA4B7 /* CenterTextTableViewCell.swift in Sources */, + F660FDEC28692EB10027C759 /* FingerprintModel.swift in Sources */, + F660FDEE28693D020027C759 /* FingerprintInfoModel.swift in Sources */, F63B9228279BC31B007E845F /* HostHistoryList.swift in Sources */, F69E365623580AA00017296E /* DataFeedService.swift in Sources */, F68CE5622302203B0068D104 /* CopyDetailCell.swift in Sources */, @@ -832,7 +898,9 @@ F6A4EB9B2301E78E00439562 /* DNSResolver.swift in Sources */, F65FDA6527AB03BC0024521B /* WhoisXmlReputationSectionModel.swift in Sources */, F65FDA6727AB06920024521B /* WhoisXmlReputationRecord.swift in Sources */, + F660FDDE2868EAAC0027C759 /* CarrierInfoModel.swift in Sources */, F644A103279284EE0061C98B /* GoogleWebRiskSectionModel.swift in Sources */, + F660FDE2286900000027C759 /* ProcessInfoModel.swift in Sources */, F650F745233CEB4B00BAA609 /* DataFeedCells.swift in Sources */, F63B922A279C8EE5007E845F /* HostResultSection.swift in Sources */, F619760722FCA33B00CE39A0 /* WhoisXmlCells.swift in Sources */, @@ -844,6 +912,7 @@ F668EB4B2356AE3800EEB379 /* IAPFooterView.swift in Sources */, F668EB3C23501FA200EEB379 /* DataFeedEndpoint.swift in Sources */, F68CE56023021AC70068D104 /* CopyLabel.swift in Sources */, + F660FDE6286902AF0027C759 /* MemoryInfoModel.swift in Sources */, F64418E6271CA13000E40F2C /* LocalDns.swift in Sources */, F6D4BB0C23584E0C00FDAFF7 /* GoogleWebRiskCellManager.swift in Sources */, F650F749233CF15800BAA609 /* DataFeed.swift in Sources */, @@ -864,6 +933,7 @@ F650F747233CECD900BAA609 /* DataFeedCell.swift in Sources */, F643030B25E47BA300109C92 /* EZPanel.swift in Sources */, F68CE5682303291A0068D104 /* Error.swift in Sources */, + F660FDF0286953A00027C759 /* DeviceInfoSectionView.swift in Sources */, F644A0FA279268BD0061C98B /* FSDisclosureGroup.swift in Sources */, F63102772130D76200F34E42 /* ReachabilityViewController.swift in Sources */, F644A0F827925CF60061C98B /* HostModelWrapperView.swift in Sources */, @@ -873,8 +943,10 @@ F6397FF225EF012300914B3F /* ShareSheetView.swift in Sources */, F60FD36525E060A800BCEF86 /* Persistence.swift in Sources */, F6397FE525E8937A00914B3F /* HostView.swift in Sources */, + F660FDEA28692CFB0027C759 /* WebkitOverlayView.swift in Sources */, F6FDDFC423047E7E00FBF75E /* SettingsViewController.swift in Sources */, F69E364A235806370017296E /* WhoisXMLDnsService.swift in Sources */, + F660FDDB2868EA8C0027C759 /* DeviceInfoSectionModel.swift in Sources */, F6FDDFE0230C4C7400FBF75E /* CellManager.swift in Sources */, F6A6410E2844246100E00F7F /* HostViewSectionFocusView.swift in Sources */, F6397FF725EF045200914B3F /* PurchaseCellView.swift in Sources */, diff --git a/ec3730/Controllers/SRCTabBarController.swift b/ec3730/Controllers/SRCTabBarController.swift index c6cdbf2..f14d8d8 100644 --- a/ec3730/Controllers/SRCTabBarController.swift +++ b/ec3730/Controllers/SRCTabBarController.swift @@ -43,7 +43,12 @@ class SRCTabBarController: SplitTabBarViewController { host.tabBarItem = UITabBarItem(title: "Host", image: UIImage(named: "Network"), tag: 3) host.tabBarItem.selectedImage = UIImage(named: "Network_selected") - let device = DeviceViewController() + var device: UIViewController! + if #available(iOS 15.0, *) { + device = UIHostingController(rootView: HostModelWrapperView(view: DeviceInfoView())) + } else { + device = DeviceViewController() + } device.tabBarItem = UITabBarItem(title: "Device", image: UIImage(named: "Device"), tag: 3) device.tabBarItem.selectedImage = UIImage(named: "Device_selected") diff --git a/ec3730/Models/Device/DeviceInfoModel.swift b/ec3730/Models/Device/DeviceInfoModel.swift new file mode 100644 index 0000000..1743481 --- /dev/null +++ b/ec3730/Models/Device/DeviceInfoModel.swift @@ -0,0 +1,56 @@ +import Combine +import Foundation + +/* + case 1: return "Memory" + case 2: return "JavaScriptCore" + case 3: return "Fingerprints" + case 4: + return "Cellular Providers" + */ + +class DeviceInfoModel: ObservableObject { + @Published var sections = [DeviceInfoSectionModel]() + + init() { + Task { @MainActor in + sections.append(UIDeviceInfoModel()) + sections.append(ProcessInfoModel()) + sections.append(MemoryInfoModel()) + sections.append(JavaScriptInfoModel()) + sections.append(CarrierInfoModel()) + sections.append(FingerprintInfoModel()) + self.reload() + } + } + + @MainActor func reload() { + objectWillChange.send() + sections.forEach { section in + section.objectWillChange.send() + section.reload() + } + } + + @MainActor func reloadFingerprints() { + guard let finger = sections.first(where: { $0 as? FingerprintInfoModel != nil }) as? FingerprintInfoModel else { + return + } + objectWillChange.send() + finger.objectWillChange.send() + finger.reload() + } + + @MainActor func attachFingerprint(model: FingerPrintModel) { + guard let finger = sections.first(where: { $0 as? FingerprintInfoModel != nil }) as? FingerprintInfoModel else { + return + } + objectWillChange.send() + model.parent = self + finger.attachModel(model: model) + Task { @MainActor in + try await Task.sleep(nanoseconds: 1000) + reload() + } + } +} diff --git a/ec3730/Models/Device/DeviceInfoSectionModel.swift b/ec3730/Models/Device/DeviceInfoSectionModel.swift new file mode 100644 index 0000000..a6fdd09 --- /dev/null +++ b/ec3730/Models/Device/DeviceInfoSectionModel.swift @@ -0,0 +1,9 @@ +import SwiftUI + +class DeviceInfoSectionModel: ObservableObject, Identifiable { + var title: String = "" + @MainActor @Published var enabled: Bool = false + @MainActor @Published var rows = [CopyCellView]() + + @MainActor func reload() {} +} diff --git a/ec3730/Models/Device/Sections/CarrierInfoModel.swift b/ec3730/Models/Device/Sections/CarrierInfoModel.swift new file mode 100644 index 0000000..02aed1e --- /dev/null +++ b/ec3730/Models/Device/Sections/CarrierInfoModel.swift @@ -0,0 +1,74 @@ +import CoreTelephony +import SwiftUI + +class CarrierInfoModel: DeviceInfoSectionModel { + private var getProviderTask: Task? + private var networkInfo: CTTelephonyNetworkInfo? + private var providers: [String: CTCarrier]? + + override init() { + super.init() + title = "Cellular Providers" + + Task { + await reload() + } + + NotificationCenter.default.addObserver(self, selector: #selector(reload), name: NSNotification.Name.CTServiceRadioAccessTechnologyDidChange, object: nil) + } + + @MainActor private func setEnabled(_ completion: (@MainActor() -> Void)? = nil) { + getProviderTask?.cancel() + getProviderTask = Task.detached(priority: .userInitiated) { + self.networkInfo = CTTelephonyNetworkInfo() + self.providers = self.networkInfo?.serviceSubscriberCellularProviders + + Task { @MainActor [weak self] in + self?.objectWillChange.send() + self?.enabled = (self?.providers?.count ?? 0) > 0 + completion?() + } + } + } + + @MainActor private func setRows() { + rows.removeAll() + + guard enabled, let networkInfo = networkInfo, let providers = providers else { + return + } + + if let value = networkInfo.dataServiceIdentifier { + rows.append(CopyCellView(title: "Data Service Identifier", content: value)) + } + + if let value = networkInfo.serviceCurrentRadioAccessTechnology { + for item in value { + rows.append(CopyCellView(title: "Data Service \(item.key) Radio Access Technology", content: item.value)) + } + } + + for (i, carrier) in providers.enumerated() { + if let name = carrier.value.carrierName { + rows.append(CopyCellView(title: "Provider \(i) Carrier Name", content: name)) + } + rows.append(CopyCellView(title: "Provider \(i) Service", content: carrier.key)) + rows.append(CopyCellView(title: "Provider \(i) Allows VOIP", content: carrier.value.allowsVOIP ? "Yes" : "No")) + if let value = carrier.value.isoCountryCode { + rows.append(CopyCellView(title: "Provider \(i) ISO Country Code", content: value)) + } + if let value = carrier.value.mobileCountryCode { + rows.append(CopyCellView(title: "Provider \(i) Mobile Country Code", content: value)) + } + if let value = carrier.value.mobileNetworkCode { + rows.append(CopyCellView(title: "Provider \(i) Mobile Network Code", content: value)) + } + } + } + + @objc @MainActor override func reload() { + setEnabled { [weak self] in + self?.setRows() + } + } +} diff --git a/ec3730/Models/Device/Sections/FingerprintInfoModel.swift b/ec3730/Models/Device/Sections/FingerprintInfoModel.swift new file mode 100644 index 0000000..f5f5d08 --- /dev/null +++ b/ec3730/Models/Device/Sections/FingerprintInfoModel.swift @@ -0,0 +1,32 @@ +import Combine +import DeviceKit +import SwiftUI + +class FingerprintInfoModel: DeviceInfoSectionModel { + var models = [FingerPrintModel]() + + override init() { + super.init() + title = "Fingerprints" + + Task { @MainActor in + reload() + } + } + + @MainActor func attachModel(model: FingerPrintModel) { + if !models.contains(model) { + models.append(model) + reload() + } + } + + @MainActor override func reload() { + enabled = models.count > 0 + rows.removeAll() + + for (i, model) in models.enumerated() { + rows.append(CopyCellView(title: "Fingerprint \(i)", content: model.fingerprint ?? "-")) + } + } +} diff --git a/ec3730/Models/Device/Sections/JavaScriptInfoModel.swift b/ec3730/Models/Device/Sections/JavaScriptInfoModel.swift new file mode 100644 index 0000000..7d70703 --- /dev/null +++ b/ec3730/Models/Device/Sections/JavaScriptInfoModel.swift @@ -0,0 +1,79 @@ +import Combine +import JavaScriptCore +import SwiftUI + +class JavaScriptInfoModel: DeviceInfoSectionModel { + override init() { + super.init() + title = "JavaScriptCore Information" + + Task { @MainActor in + reload() + } + } + + @MainActor override func reload() { + enabled = true + rows.removeAll() + + guard let context = JSContext() else { + return + } + + if let pi = context.evaluateScript("Math.PI"), pi.isNumber { + rows.append(CopyCellView(title: "PI", content: "\(pi.toDouble())")) + } + + if let value = context.evaluateScript("Math.E"), value.isNumber { + rows.append(CopyCellView(title: "Euler's constant", content: "\(value.toDouble())")) + } + + if let value = context.evaluateScript("Math.random()"), value.isNumber { + rows.append(CopyCellView(title: "Random", content: "\(value.toDouble())")) + } + + if let value = context.evaluateScript("Math.log(2)"), value.isNumber { + rows.append(CopyCellView(title: "Natural log of 2", content: "\(value.toDouble())")) + } + + if let value = context.evaluateScript("Math.log(10)"), value.isNumber { + rows.append(CopyCellView(title: "Natural log of 10", content: "\(value.toDouble())")) + } + + if let value = context.evaluateScript("Math.log2(10)"), value.isNumber { + rows.append(CopyCellView(title: "Base 2 logarithm of 10", content: "\(value.toDouble())")) + } + + if let value = context.evaluateScript("Math.log2(Math.E)"), value.isNumber { + rows.append(CopyCellView(title: "Base 2 logarithm of E", content: "\(value.toDouble())")) + } + + if let value = context.evaluateScript("Math.sqrt(2)"), value.isNumber { + rows.append(CopyCellView(title: "Square root of 2", content: "\(value.toDouble())")) + } + + if let value = context.evaluateScript("Math.sqrt(1/2)"), value.isNumber { + rows.append(CopyCellView(title: "Square root of 1/2", content: "\(value.toDouble())")) + } + + if let value = context.evaluateScript("Number.MAX_SAFE_INTEGER"), value.isNumber, let str = value.toNumber()?.stringValue { + rows.append(CopyCellView(title: "Maxmimum Safe Integer", content: str)) + } + + if let value = context.evaluateScript("Number.MIN_SAFE_INTEGER"), value.isNumber, let str = value.toNumber()?.stringValue { + rows.append(CopyCellView(title: "Minimum Safe Integer", content: str)) + } + + if let value = context.evaluateScript("Number.EPSILON"), value.isNumber, let str = value.toNumber()?.stringValue { + rows.append(CopyCellView(title: "Epsilon", content: str)) + } + + if let value = context.evaluateScript("Number.MAX_VALUE"), value.isNumber { + rows.append(CopyCellView(title: "Maximum Value", content: "\(value.toDouble())")) + } + + if let value = context.evaluateScript("Number.MIN_VALUE"), value.isNumber { + rows.append(CopyCellView(title: "Minimum Value", content: "\(value.toDouble())")) + } + } +} diff --git a/ec3730/Models/Device/Sections/MemoryInfoModel.swift b/ec3730/Models/Device/Sections/MemoryInfoModel.swift new file mode 100644 index 0000000..37df5ea --- /dev/null +++ b/ec3730/Models/Device/Sections/MemoryInfoModel.swift @@ -0,0 +1,91 @@ +import Combine +import MachO +import SwiftUI + +class MemoryInfoModel: DeviceInfoSectionModel { + override init() { + super.init() + title = "Memory Information" + + Task { @MainActor in + reload() + } + } + + func memoryFootprint() -> Float? { + // The `TASK_VM_INFO_COUNT` and `TASK_VM_INFO_REV1_COUNT` macros are too + // complex for the Swift C importer, so we have to define them ourselves. + let TASK_VM_INFO_COUNT = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) + let TASK_VM_INFO_REV1_COUNT = mach_msg_type_number_t(MemoryLayout.offset(of: \task_vm_info_data_t.min_address)! / MemoryLayout.size) + var info = task_vm_info_data_t() + var count = TASK_VM_INFO_COUNT + let kr = withUnsafeMutablePointer(to: &info) { infoPtr in + infoPtr.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { intPtr in + task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), intPtr, &count) + } + } + guard + kr == KERN_SUCCESS, + count >= TASK_VM_INFO_REV1_COUNT + else { return nil } + + let usedBytes = Float(info.phys_footprint) + return usedBytes + } + + func formattedMemoryFootprint() -> String { + let usedBytes: UInt64? = UInt64(memoryFootprint() ?? 0) + let usedMB = Double(usedBytes ?? 0) / 1024 / 1024 + let usedMBAsString = String(format: "%0.02f MB", usedMB) + return usedMBAsString + } + + /// https://github.com/PerfectlySoft/Perfect-SysInfo/blob/master/Sources/PerfectSysInfo/PerfectSysInfo.swift#L359 + func vm_stat() -> [String: Int] { + let size = MemoryLayout.size / MemoryLayout.size + let pStat = UnsafeMutablePointer.allocate(capacity: size) + var stat: [String: Int] = [:] + var count = mach_msg_type_number_t(size) + if host_statistics(mach_host_self(), HOST_VM_INFO, pStat, &count) == 0 { + let array = Array(UnsafeBufferPointer(start: pStat, count: size)) + let cnt = min(tags.count, array.count) + for i in 0 ... cnt - 1 { + let key = tags[i] + let value = array[i] + stat[key] = Int(value) / 256 + } // next i + } // end if + pStat.deallocate() + return stat + } + + func ggsdf() -> (kern_return_t, vm_size_t) { + var pageSize: vm_size_t = 0 + let result = withUnsafeMutablePointer(to: &pageSize) { size -> kern_return_t in + host_page_size(mach_host_self(), size) + } + + return (result, pageSize) + } + + let tags = ["free", "active", "inactive", "wired", "zero_filled", "reactivations", "pageins", "pageouts", "faults", "cow", "lookups", "hits"] + + @MainActor override func reload() { + enabled = true + rows.removeAll() + + rows.append(CopyCellView(title: "Memory Footprint", content: formattedMemoryFootprint())) + + let (kern_result, page_size) = ggsdf() + if kern_result == KERN_SUCCESS { + rows.append(CopyCellView(title: "Page Size", content: "\(page_size) bytes")) + } + let stats = vm_stat() + + for key in tags { + if let val = stats[key] { + rows.append(CopyCellView(title: key, content: String(format: "%d MB", val))) + } + } + } +} diff --git a/ec3730/Models/Device/Sections/ProcessInfoModel.swift b/ec3730/Models/Device/Sections/ProcessInfoModel.swift new file mode 100644 index 0000000..8df5f12 --- /dev/null +++ b/ec3730/Models/Device/Sections/ProcessInfoModel.swift @@ -0,0 +1,59 @@ +import Combine +import DeviceKit +import MachO +import SwiftUI + +extension ProcessInfo.ThermalState: CustomStringConvertible { + public var description: String { + switch self { + case .critical: + return "Critical" + case .nominal: + return "Nominal" + case .fair: + return "Fair" + case .serious: + return "Serious" + @unknown default: + return "Unknown" + } + } +} + +class ProcessInfoModel: DeviceInfoSectionModel { + override init() { + super.init() + title = "Process Information" + + Task { @MainActor in + reload() + } + } + + @MainActor override func reload() { + enabled = true + rows.removeAll() + + let info = ProcessInfo.processInfo + + rows.append(CopyCellView(title: "Proces Name", content: info.processName)) + rows.append(CopyCellView(title: "Active Processors", content: "\(info.activeProcessorCount)")) + rows.append(CopyCellView(title: "Hostname", content: "\(info.hostName)")) + rows.append(CopyCellView(title: "Process Arguments", content: "\(info.arguments.joined(separator: " "))")) + rows.append(CopyCellView(title: "Low Power Mode", content: info.isLowPowerModeEnabled ? "Enabled" : "Disabled")) + rows.append(CopyCellView(title: "Physical Memory", content: "\(info.physicalMemory) bytes")) + rows.append(CopyCellView(title: "Globally Unique String", content: "\(info.globallyUniqueString)")) + rows.append(CopyCellView(title: "OS Version", content: info.operatingSystemVersionString)) + rows.append(CopyCellView(title: "System Uptime", content: "\(info.systemUptime)")) + rows.append(CopyCellView(title: "Is Mac Catalyst App", content: info.isMacCatalystApp ? "Yes" : "No")) + rows.append(CopyCellView(title: "Is iOS App on Mac", content: info.isiOSAppOnMac ? "Yes" : "No")) + rows.append(CopyCellView(title: "Prcoess Identifier (PID)", content: "\(info.processIdentifier)")) + rows.append(CopyCellView(title: "Thermal State", content: info.thermalState.description)) + + func getArchitecture() -> NSString { + let info = NXGetLocalArchInfo() + return NSString(utf8String: (info?.pointee.description)!)! + } + rows.append(CopyCellView(title: "Architecture", content: getArchitecture() as String)) + } +} diff --git a/ec3730/Models/Device/Sections/UIDeviceInfoModel.swift b/ec3730/Models/Device/Sections/UIDeviceInfoModel.swift new file mode 100644 index 0000000..b94a512 --- /dev/null +++ b/ec3730/Models/Device/Sections/UIDeviceInfoModel.swift @@ -0,0 +1,58 @@ +import Combine +import DeviceKit +import SwiftUI + +/// Device information from ``UIDevice`` and ``DeviceKit`` +class UIDeviceInfoModel: DeviceInfoSectionModel { + override init() { + super.init() + title = "Device Information" + + Task { @MainActor in + reload() + } + } + + @MainActor override func reload() { + enabled = true + rows.removeAll() + + rows.append(CopyCellView(title: "Model", content: UIDevice.current.model)) + rows.append(CopyCellView(title: "Localized Model", content: UIDevice.current.localizedModel)) + rows.append(CopyCellView(title: "Name", content: UIDevice.current.name)) + rows.append(CopyCellView(title: "System Name", content: UIDevice.current.systemName)) + rows.append(CopyCellView(title: "System Version", content: UIDevice.current.systemVersion)) + rows.append(CopyCellView(title: "UUID", content: UIDevice.current.identifierForVendor?.uuidString ?? "?")) + rows.append(CopyCellView(title: "Idiom", content: UIDevice.current.userInterfaceIdiom.description ?? "?")) + rows.append(CopyCellView(title: "Hardware Model", content: UIDevice.current.hwModel)) + rows.append(CopyCellView(title: "Hardware Machine", content: UIDevice.current.hwMachine)) + rows.append(CopyCellView(title: "Disk Space Available", content: "\(UIDevice.current.freeDiskSpaceInBytes) bytes")) + rows.append(CopyCellView(title: "Total Disk Space", content: "\(UIDevice.current.totalDiskSpaceInBytes) bytes")) + rows.append(CopyCellView(title: "Supports Multitasking", content: UIDevice.current.isMultitaskingSupported ? "Yes" : "No")) + + if let bootTime = UIDevice.current.boottime { + rows.append(CopyCellView(title: "Boot time", content: "\(bootTime)")) + rows.append(CopyCellView(title: "Total Uptime", content: "\(UIDevice.current.uptime)")) + } + + if UIDevice.current.batteryLevel >= 0, UIDevice.current.batteryState != .unknown, UIDevice.current.isBatteryMonitoringEnabled { + rows.append(CopyCellView(title: "Battery Level", content: "\(UIDevice.current.batteryLevel * 100)%")) + rows.append(CopyCellView(title: "Battery State", content: "\(UIDevice.current.batteryState.description ?? "?")")) + } else { + rows.append(CopyCellView(title: "Battery", content: "Battery monitoring is not enabled")) + } + + rows.append(CopyCellView(title: "Device", content: "\(Device.current.safeDescription)")) + rows.append(CopyCellView(title: "Supports 3D Touch", content: Device.current.has3dTouchSupport ? "Yes" : "No")) + rows.append(CopyCellView(title: "Has Biometric Sensor", content: Device.current.hasBiometricSensor ? "Yes" : "No")) + rows.append(CopyCellView(title: "Diagonal Length", content: "\(Device.current.diagonal) inches")) + rows.append(CopyCellView(title: "Brightness", content: "\(Device.current.screenBrightness)%")) + rows.append(CopyCellView(title: "Has Lidar", content: Device.current.hasLidarSensor ? "Yes" : "No")) + rows.append(CopyCellView(title: "Has Camera", content: Device.current.hasCamera ? "Yes" : "No")) + rows.append(CopyCellView(title: "Has Wide Camera", content: Device.current.hasWideCamera ? "Yes" : "No")) + rows.append(CopyCellView(title: "Has Sensor Housing", content: Device.current.hasSensorHousing ? "Yes" : "No")) + rows.append(CopyCellView(title: "Has Telephoto Camera", content: Device.current.hasTelephotoCamera ? "Yes" : "No")) + rows.append(CopyCellView(title: "Has Ultrawide Camera", content: Device.current.hasUltraWideCamera ? "Yes" : "No")) + rows.append(CopyCellView(title: "Has Rounded Display Corners", content: Device.current.hasRoundedDisplayCorners ? "Yes" : "No")) + } +} diff --git a/ec3730/Models/FingerprintModel.swift b/ec3730/Models/FingerprintModel.swift new file mode 100644 index 0000000..18dd5e6 --- /dev/null +++ b/ec3730/Models/FingerprintModel.swift @@ -0,0 +1,37 @@ +import Foundation +import WebKit + +class FingerPrintModel: ObservableObject { + var url: URL + var _onFinish: (FingerPrintModel, WKWebView) -> Void + weak var parent: DeviceInfoModel? + + @Published var fingerprint: String? + + init(_ url: URL, onFinish: @escaping (FingerPrintModel, WKWebView) -> Void) { + self.url = url + _onFinish = onFinish + } + + func reload(_ webview: WKWebView) { + fingerprint = nil + webview.load(URLRequest(url: url)) + } + + func onFinish(_ model: FingerPrintModel, _ webview: WKWebView) { + _onFinish(model, webview) + } + + func update(fingerprint: String) { + Task { @MainActor in + self.fingerprint = fingerprint + self.parent?.reloadFingerprints() + } + } +} + +extension FingerPrintModel: Equatable { + static func == (lhs: FingerPrintModel, rhs: FingerPrintModel) -> Bool { + lhs.url == rhs.url + } +} diff --git a/ec3730/Views/Device/DeviceInfoSectionView.swift b/ec3730/Views/Device/DeviceInfoSectionView.swift new file mode 100644 index 0000000..7a9b067 --- /dev/null +++ b/ec3730/Views/Device/DeviceInfoSectionView.swift @@ -0,0 +1,53 @@ +import SwiftUI + +struct DeviceInfoSectionView: View { + var section: DeviceInfoSectionModel + @AppStorage var isExpanded: Bool + @State var focused = false + + init(section: DeviceInfoSectionModel) { + self.section = section + _isExpanded = AppStorage(wrappedValue: true, "\(section.title).deviceinfo.isExpanded") + } + + var body: some View { + FSDisclosureGroup(isExpanded: $isExpanded, content: { + ForEach(section.rows) { row in + row + } + }, label: { + HStack(alignment: .center) { + Text(section.title).font(.headline).padding() + Spacer() + } + }) + .background(Color(UIColor.systemGroupedBackground)) + .contextMenu { + Button(action: { + withAnimation { + self.isExpanded.toggle() + } + }, label: { + Label(self.isExpanded ? "Collapse" : "Expand", systemImage: self.isExpanded ? "rectangle.compress.vertical" : "rectangle.expand.vertical") + }) + Button(action: { + withAnimation { + self.focused.toggle() + } + }, label: { + Label("Focus", systemImage: "rectangle.and.text.magnifyingglass") + }) + } + .sheet(isPresented: $focused, content: { + EZPanel(content: { + ScrollView { + ForEach(section.rows) { row in + row + } + } + .navigationTitle(section.title) + .navigationBarTitleDisplayMode(.inline) + }) + }) + } +} diff --git a/ec3730/Views/Device/DeviceInfoView.swift b/ec3730/Views/Device/DeviceInfoView.swift new file mode 100644 index 0000000..2f6393f --- /dev/null +++ b/ec3730/Views/Device/DeviceInfoView.swift @@ -0,0 +1,92 @@ +import SwiftUI + +struct DeviceInfoView: View { + @StateObject var model = DeviceInfoModel() + @StateObject var fingerprint = FingerPrintModel( + URL(staticString: "https://fingerprint.netutils.workers.dev/"), + onFinish: { model, webView in + webView.evaluateJavaScript("document.documentElement.innerText.toString()") { text, _ in + guard let htmlString = text as? String else { + return + } + + Task { @MainActor in + model.update(fingerprint: htmlString) + } + } + } + ) + @StateObject var fingerprintTwo = FingerPrintModel( + URL(staticString: "https://fingerprint2.netutils.workers.dev/"), + onFinish: { model, webView in + webView.evaluateJavaScript("document.documentElement.innerText.toString()") { json, _ in + guard let json = json as? String, let jsonData = json.data(using: .utf8) else { + return + } + guard let d = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { + return + } + if let htmlString = d["hash"] as? String { + model.update(fingerprint: htmlString) + } + } + } + ) + @State private var date = Date.now + + var body: some View { + ScrollView { + LazyVStack(spacing: 0) { + ForEach(model.sections) { section in + Group { + if section.enabled { + DeviceInfoSectionView(section: section) + } + } + } + Divider() + Text("Last Updated \(date.ISO8601Format(.iso8601))") + .font(.footnote) + .foregroundColor(.gray) + .frame(alignment: .center) + .padding() + } + .id(date) + } + .background { + ZStack { + WebkitOverlayView(model: fingerprint) + .allowsHitTesting(false) + WebkitOverlayView(model: fingerprintTwo) + .allowsHitTesting(false) + } + } + .navigationBarTitle("Device") + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { + withAnimation { + date = .now + model.reload() + } + } label: { + Label("Reload", systemImage: "arrow.clockwise") + } + } + } + .onAppear { + Task { + model.attachFingerprint(model: fingerprint) + model.attachFingerprint(model: fingerprintTwo) + } + } + } +} + +#if DEBUG + struct DeviceInfoViewPreview: PreviewProvider { + static var previews: some View { + DeviceInfoView() + } + } +#endif diff --git a/ec3730/Views/WebkitOverlayView.swift b/ec3730/Views/WebkitOverlayView.swift new file mode 100644 index 0000000..20e5a6b --- /dev/null +++ b/ec3730/Views/WebkitOverlayView.swift @@ -0,0 +1,37 @@ +import SwiftUI +import WebKit + +struct WebkitOverlayView: UIViewRepresentable { + @ObservedObject var model: FingerPrintModel + + func makeUIView(context: Context) -> WKWebView { + let webView = WKWebView() + webView.frame = .init(x: 0, y: 0, width: 1, height: 1) + webView.alpha = 0.0005 + webView.navigationDelegate = context.coordinator + DispatchQueue.main.async { + model.reload(webView) + } + return webView + } + + func updateUIView(_: WKWebView, context _: Context) { + // no-op + } + + func makeCoordinator() -> Coordinator { + Coordinator(model) + } + + class Coordinator: NSObject, WKNavigationDelegate { + @ObservedObject var model: FingerPrintModel + + init(_ model: FingerPrintModel) { + self.model = model + } + + func webView(_ webView: WKWebView, didFinish _: WKNavigation!) { + model.onFinish(model, webView) + } + } +}