ClassifyPhotoPlugin.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. import Flutter
  2. import StoreKit
  3. import Photos
  4. import UIKit
  5. public class ClassifyPhotoPlugin: NSObject, FlutterPlugin {
  6. var photoClassifier = ClassifyPhoto()
  7. public static func register(with registrar: FlutterPluginRegistrar) {
  8. let channel = FlutterMethodChannel(name: "classify_photo", binaryMessenger: registrar.messenger())
  9. let instance = ClassifyPhotoPlugin()
  10. registrar.addMethodCallDelegate(instance, channel: channel)
  11. }
  12. public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
  13. print("iOS: Received method call: \(call.method)")
  14. switch call.method {
  15. case "getPhoto":
  16. self.getPhoto(flutterResult: result)
  17. case "getStorageInfo":
  18. getStorageInfo(result: result)
  19. case "checkTrialEligibility":
  20. if #available(iOS 15.0, *) {
  21. Task {
  22. let handler = SubscriptionHandler()
  23. let isEligible = await handler.checkTrialEligibility()
  24. DispatchQueue.main.async {
  25. result(isEligible)
  26. }
  27. }
  28. }
  29. case "getExifInfo":
  30. guard let args = call.arguments as? [String: Any],
  31. let filePath = args["filePath"] as? String else {
  32. result(FlutterError(
  33. code: "INVALID_ARGUMENTS",
  34. message: "Missing filePath parameter",
  35. details: nil
  36. ))
  37. return
  38. }
  39. getExifInfo(filePath: filePath, completion: result)
  40. case "finishTran":
  41. let handler = SubscriptionHandler()
  42. handler.finishTransaction()
  43. result(nil)
  44. case "getPlatformVersion":
  45. result("iOS " + UIDevice.current.systemVersion)
  46. default:
  47. result(FlutterMethodNotImplemented)
  48. }
  49. }
  50. private class func blankof<T>(type:T.Type) -> T {
  51. let ptr = UnsafeMutablePointer<T>.allocate(capacity: MemoryLayout<T>.size)
  52. let val = ptr.pointee
  53. return val
  54. }
  55. /// 磁盘总大小
  56. private class func getTotalDiskSize() -> Int64 {
  57. var fs = blankof(type: statfs.self)
  58. if statfs("/var",&fs) >= 0{
  59. return Int64(UInt64(fs.f_bsize) * fs.f_blocks)
  60. }
  61. return -1
  62. }
  63. private func getStorageInfo(result: @escaping FlutterResult) {
  64. DispatchQueue.global(qos: .userInitiated).async {
  65. var storageInfo: [String: Int64] = [:]
  66. // 获取总容量和可用容量
  67. if let space = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()) {
  68. let totalSpace = ClassifyPhotoPlugin.getTotalDiskSize()
  69. let freeSpace = space[.systemFreeSize] as? Int64 ?? 0
  70. storageInfo["totalSpace"] = totalSpace
  71. storageInfo["freeSpace"] = freeSpace
  72. storageInfo["usedSpace"] = totalSpace - freeSpace
  73. }
  74. // 获取照片占用的空间
  75. let options = PHFetchOptions()
  76. let allPhotos = PHAsset.fetchAssets(with: .image, options: options)
  77. var photoSize: Int64 = 0
  78. let group = DispatchGroup()
  79. let queue = DispatchQueue(label: "com.app.photosize", attributes: .concurrent)
  80. let semaphore = DispatchSemaphore(value: 10) // 限制并发
  81. allPhotos.enumerateObjects { (asset, index, stop) in
  82. group.enter()
  83. semaphore.wait()
  84. let resources = PHAssetResource.assetResources(for: asset)
  85. if let resource = resources.first {
  86. queue.async {
  87. let options = PHAssetResourceRequestOptions()
  88. options.isNetworkAccessAllowed = true
  89. PHAssetResourceManager.default().requestData(
  90. for: resource,
  91. options: options,
  92. dataReceivedHandler: { data in
  93. photoSize += Int64(data.count)
  94. },
  95. completionHandler: { error in
  96. if let error = error {
  97. print("Error getting photo size: \(error)")
  98. }
  99. semaphore.signal()
  100. group.leave()
  101. }
  102. )
  103. }
  104. } else {
  105. semaphore.signal()
  106. group.leave()
  107. }
  108. }
  109. group.notify(queue: .main) {
  110. storageInfo["photoSpace"] = photoSize
  111. result(storageInfo)
  112. }
  113. }
  114. }
  115. private func getPhoto(flutterResult: @escaping FlutterResult) {
  116. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  117. guard let self = self else { return }
  118. let fetchOptions = PHFetchOptions()
  119. let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
  120. photoClassifier.classifyPhotos(
  121. assets: allPhotos,
  122. progressHandler: { (stage, progress) in
  123. print("Progress: \(stage) - \(progress)")
  124. },
  125. completion: { result in
  126. var resultData: [[String: Any]] = []
  127. let mainGroup = DispatchGroup()
  128. // 处理截图
  129. mainGroup.enter()
  130. self.processPhotoGroup(assets: result.screenshots, groupName: "screenshots", sizeInfo: result.screenshotsSize) { groupData in
  131. if !groupData.isEmpty {
  132. resultData.append(["group": groupData, "type": "screenshots"])
  133. }
  134. mainGroup.leave()
  135. }
  136. // 处理相似照片组
  137. for photoGroup in result.similarPhotos {
  138. mainGroup.enter()
  139. self.processPhotoGroup(assets: photoGroup, groupName: "similar", sizeInfo: result.similarPhotosSize) { groupData in
  140. if !groupData.isEmpty {
  141. resultData.append(["group": groupData, "type": "similar"])
  142. }
  143. mainGroup.leave()
  144. }
  145. }
  146. // 处理地点分组
  147. for (location, assets) in result.locations {
  148. mainGroup.enter()
  149. self.processPhotoGroup(assets: assets, groupName: location, sizeInfo: result.locationsSize) { groupData in
  150. if !groupData.isEmpty {
  151. resultData.append(["group": groupData, "type": "location", "name": location])
  152. }
  153. mainGroup.leave()
  154. }
  155. }
  156. // 处理人物分组
  157. for (person, assets) in result.people {
  158. mainGroup.enter()
  159. self.processPhotoGroup(assets: assets, groupName: person, sizeInfo: result.peopleSize) { groupData in
  160. if !groupData.isEmpty {
  161. resultData.append(["group": groupData, "type": "people"])
  162. }
  163. mainGroup.leave()
  164. }
  165. }
  166. // 处理模糊照片
  167. mainGroup.enter()
  168. self.processPhotoGroup(assets: result.blurryPhotos, groupName: "blurry", sizeInfo: result.blurryPhotosSize) { groupData in
  169. if !groupData.isEmpty {
  170. resultData.append(["group": groupData, "type": "blurry"])
  171. }
  172. mainGroup.leave()
  173. }
  174. mainGroup.notify(queue: .main) {
  175. print("Final result count: \(resultData.count)")
  176. flutterResult(resultData)
  177. }
  178. }
  179. )
  180. }
  181. }
  182. // 处理照片组的辅助方法
  183. private func processPhotoGroup(
  184. assets: [PHAsset],
  185. groupName: String,
  186. sizeInfo: ClassifyPhoto.PhotoSizeInfo,
  187. completion: @escaping ([String: Any]) -> Void
  188. ) {
  189. let photoProcessGroup = DispatchGroup()
  190. var photosData: [[String: Any]] = []
  191. for asset in assets {
  192. photoProcessGroup.enter()
  193. let options = PHContentEditingInputRequestOptions()
  194. options.isNetworkAccessAllowed = true
  195. asset.requestContentEditingInput(with: options) { (input, info) in
  196. defer { photoProcessGroup.leave() }
  197. if let input = input, let url = input.fullSizeImageURL {
  198. let photoInfo: [String: Any] = [
  199. "path": url.path,
  200. "id": asset.localIdentifier,
  201. "width": asset.pixelWidth,
  202. "height": asset.pixelHeight,
  203. "creationDate": asset.creationDate?.timeIntervalSince1970 ?? 0
  204. ]
  205. photosData.append(photoInfo)
  206. }
  207. }
  208. }
  209. photoProcessGroup.notify(queue: .main) {
  210. completion([
  211. "photos": photosData,
  212. "totalSize": sizeInfo.totalSize,
  213. "count": sizeInfo.count
  214. ])
  215. }
  216. }
  217. private func getExifInfo(filePath: String, completion: @escaping FlutterResult) {
  218. // 创建文件 URL
  219. let fileURL: URL
  220. if filePath.starts(with: "file://") {
  221. guard let url = URL(string: filePath) else {
  222. print("Invalid URL string: \(filePath)")
  223. completion([:])
  224. return
  225. }
  226. fileURL = url
  227. } else {
  228. fileURL = URL(fileURLWithPath: filePath)
  229. }
  230. // 检查文件是否存在
  231. guard FileManager.default.fileExists(atPath: fileURL.path) else {
  232. print("File does not exist at path: \(fileURL.path)")
  233. completion([:])
  234. return
  235. }
  236. // 创建图片源
  237. guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil) else {
  238. print("Failed to create image source for path: \(fileURL.path)")
  239. completion([:])
  240. return
  241. }
  242. var exifInfo: [String: Any] = [:]
  243. // 获取所有元数据
  244. if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any] {
  245. // EXIF 数据
  246. if let exif = imageProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] {
  247. exifInfo["aperture"] = exif[kCGImagePropertyExifFNumber as String]
  248. exifInfo["exposureTime"] = exif[kCGImagePropertyExifExposureTime as String]
  249. exifInfo["iso"] = exif[kCGImagePropertyExifISOSpeedRatings as String]
  250. exifInfo["focalLength"] = exif[kCGImagePropertyExifFocalLength as String]
  251. exifInfo["dateTimeOriginal"] = exif[kCGImagePropertyExifDateTimeOriginal as String]
  252. }
  253. // TIFF 数据
  254. if let tiff = imageProperties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] {
  255. exifInfo["make"] = tiff[kCGImagePropertyTIFFMake as String]
  256. exifInfo["model"] = tiff[kCGImagePropertyTIFFModel as String]
  257. }
  258. // GPS 数据
  259. if let gps = imageProperties[kCGImagePropertyGPSDictionary as String] as? [String: Any] {
  260. exifInfo["latitude"] = gps[kCGImagePropertyGPSLatitude as String]
  261. exifInfo["longitude"] = gps[kCGImagePropertyGPSLongitude as String]
  262. exifInfo["altitude"] = gps[kCGImagePropertyGPSAltitude as String]
  263. }
  264. // 图片基本信息
  265. exifInfo["pixelWidth"] = imageProperties[kCGImagePropertyPixelWidth as String]
  266. exifInfo["pixelHeight"] = imageProperties[kCGImagePropertyPixelHeight as String]
  267. exifInfo["dpi"] = imageProperties[kCGImagePropertyDPIHeight as String]
  268. exifInfo["colorModel"] = imageProperties[kCGImagePropertyColorModel as String]
  269. exifInfo["profileName"] = imageProperties[kCGImagePropertyProfileName as String]
  270. }
  271. completion(exifInfo)
  272. }
  273. }
  274. class SubscriptionHandler: NSObject {
  275. @available(iOS 15.0.0, *)
  276. func checkTrialEligibility() async -> Bool {
  277. do {
  278. // 获取产品信息
  279. let productIds = ["clean.vip.1week"]
  280. let products = try await Product.products(for: Set(productIds))
  281. // 检查第一个产品的试用资格
  282. if let product = products.first {
  283. return await product.subscription?.isEligibleForIntroOffer ?? false
  284. }
  285. return false
  286. } catch {
  287. print("Error checking trial eligibility: \(error)")
  288. return false
  289. }
  290. }
  291. // 支付前查询自动续费的订单,将订单置为支付状态
  292. func finishTransaction() {
  293. let transactions = SKPaymentQueue.default().transactions
  294. if transactions.count > 0 {
  295. for tran in transactions {
  296. if tran.transactionState == .purchased {
  297. SKPaymentQueue.default().finishTransaction(tran)
  298. }
  299. }
  300. }
  301. }
  302. }