From 53b55aec22cedb57c050caaacfb677f524a01561 Mon Sep 17 00:00:00 2001 From: Dan Cunningham Date: Sat, 29 Jun 2024 13:54:45 -0700 Subject: [PATCH] Allows for the use of "item:imageItem" for attachments Signed-off-by: Dan Cunningham --- .../NotificationService.entitlements | 10 ++ NotificationService/NotificationService.swift | 149 +++++++++++++----- openHAB.xcodeproj/project.pbxproj | 13 ++ .../openHABWatch Extension.entitlements | 7 +- 4 files changed, 138 insertions(+), 41 deletions(-) create mode 100644 NotificationService/NotificationService.entitlements diff --git a/NotificationService/NotificationService.entitlements b/NotificationService/NotificationService.entitlements new file mode 100644 index 000000000..b6063e1b2 --- /dev/null +++ b/NotificationService/NotificationService.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.openhab.app + + + diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift index 654a59c9f..4ba3405c5 100644 --- a/NotificationService/NotificationService.swift +++ b/NotificationService/NotificationService.swift @@ -9,6 +9,8 @@ // // SPDX-License-Identifier: EPL-2.0 +import Foundation +import OpenHABCore import os.log import UniformTypeIdentifiers import UserNotifications @@ -16,7 +18,7 @@ import UserNotifications class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? - + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) @@ -24,12 +26,13 @@ class NotificationService: UNNotificationServiceExtension { var notificationActions: [UNNotificationAction] = [] let userInfo = bestAttemptContent.userInfo os_log("handleNotification userInfo %{PUBLIC}@", log: .default, type: .info, userInfo) - + // Check if the user has defined custom actions in the payload if let actionsArray = parseActions(userInfo), let category = parseCategory(userInfo) { for actionDict in actionsArray { if let action = actionDict["action"], - let title = actionDict["title"] { + let title = actionDict["title"] + { var options: UNNotificationActionOptions = [] // navigate options need to bring the app forward if action.hasPrefix("ui") { @@ -52,7 +55,7 @@ class NotificationService: UNNotificationServiceExtension { intentIdentifiers: [], options: .customDismissAction ) - UNUserNotificationCenter.current().getNotificationCategories { (existingCategories) in + UNUserNotificationCenter.current().getNotificationCategories { existingCategories in // Check if the new category already exists, this is a hash of the actions string done by the cloud service let existingCategoryIdentifiers = existingCategories.map(\.identifier) if !existingCategoryIdentifiers.contains(category) { @@ -64,19 +67,29 @@ class NotificationService: UNNotificationServiceExtension { } } } - + // check if there is an attachment to put on the notification // this should be last as we need to wait for media // TODO: we should support relative paths and try the user's openHAB (local,remote) for content if let attachmentURLString = userInfo["media-attachment-url"] as? String, let attachmentURL = URL(string: attachmentURLString) { - os_log("handleNotification downloading %{PUBLIC}@", log: .default, type: .info, attachmentURLString) - downloadAndAttachMedia(url: attachmentURL) { attachment in - if let attachment { - os_log("handleNotification attaching %{PUBLIC}@", log: .default, type: .info, attachmentURLString) - bestAttemptContent.attachments = [attachment] + if let scheme = attachmentURL.scheme { + let isItem = scheme == "item" + let downloadHandler: (URL, @escaping (UNNotificationAttachment?) -> Void) -> Void = if isItem { + downloadAndAttachItemImage } else { - os_log("handleNotification could not attach %{PUBLIC}@", log: .default, type: .info, attachmentURLString) + downloadAndAttachMedia + } + + downloadHandler(attachmentURL) { attachment in + if let attachment { + os_log("handleNotification attaching %{PUBLIC}@", log: .default, type: .info, attachmentURLString) + bestAttemptContent.attachments = [attachment] + } else { + os_log("handleNotification could not attach %{PUBLIC}@", log: .default, type: .info, attachmentURLString) + } + contentHandler(bestAttemptContent) } + } else { contentHandler(bestAttemptContent) } } else { @@ -84,7 +97,7 @@ class NotificationService: UNNotificationServiceExtension { } } } - + override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. @@ -93,7 +106,7 @@ class NotificationService: UNNotificationServiceExtension { contentHandler(bestAttemptContent) } } - + private func parseActions(_ userInfo: [AnyHashable: Any]) -> [[String: String]]? { // Extract actions and convert it from JSON string to an array of dictionaries if let actionsString = userInfo["actions"] as? String, let actionsData = actionsString.data(using: .utf8) { @@ -107,16 +120,17 @@ class NotificationService: UNNotificationServiceExtension { } return nil } - + private func parseCategory(_ userInfo: [AnyHashable: Any]) -> String? { // Extract category from aps dictionary if let aps = userInfo["aps"] as? [String: Any], - let category = aps["category"] as? String { + let category = aps["category"] as? String + { return category } return nil } - + private func downloadAndAttachMedia(url: URL, completion: @escaping (UNNotificationAttachment?) -> Void) { let task = URLSession.shared.downloadTask(with: url) { localURL, response, error in guard let localURL else { @@ -124,33 +138,88 @@ class NotificationService: UNNotificationServiceExtension { completion(nil) return } - - do { - let fileManager = FileManager.default - let tempDirectory = NSTemporaryDirectory() - let tempFile = URL(fileURLWithPath: tempDirectory).appendingPathComponent(UUID().uuidString) - - try fileManager.moveItem(at: localURL, to: tempFile) - - let attachment: UNNotificationAttachment? - - if let mimeType = response?.mimeType, - let utType = UTType(mimeType: mimeType), - utType.conforms(to: .data) { - let newTempFile = tempFile.appendingPathExtension(utType.preferredFilenameExtension ?? "") - try fileManager.moveItem(at: tempFile, to: newTempFile) - attachment = try UNNotificationAttachment(identifier: UUID().uuidString, url: newTempFile, options: nil) - } else { - os_log("Unrecognized MIME type or file extension", log: .default, type: .error) - attachment = nil + self.attachFile(localURL: localURL, mimeType: response?.mimeType, completion: completion) + } + task.resume() + } + + func downloadAndAttachItemImage(attachmentURL: URL, completion: @escaping (UNNotificationAttachment?) -> Void) { + guard let scheme = attachmentURL.scheme else { + os_log("Could not find scheme %{PUBLIC}@", log: .default, type: .info) + return + } + + let itemName = String(attachmentURL.absoluteString.dropFirst(scheme.count + 1)) + + OpenHABItemCache.instance.getItem(name: itemName) { item in + guard let item else { + os_log("Could not find item %{PUBLIC}@", log: .default, type: .info, itemName) + completion(nil) + return + } + if let state = item.state { + do { + // Extract MIME type and base64 string + let pattern = "^data:(.*?);base64,(.*)$" + let regex = try NSRegularExpression(pattern: pattern, options: []) + + if let match = regex.firstMatch(in: state, options: [], range: NSRange(location: 0, length: state.utf16.count)) { + let mimeTypeRange = Range(match.range(at: 1), in: state) + let base64Range = Range(match.range(at: 2), in: state) + + if let mimeTypeRange, let base64Range { + let mimeType = String(state[mimeTypeRange]) + let base64String = String(state[base64Range]) + if let imageData = Data(base64Encoded: base64String) { + // Create a temporary file URL + let tempDirectory = FileManager.default.temporaryDirectory + let tempFileURL = tempDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("jpeg") + do { + try imageData.write(to: tempFileURL) + os_log("Image saved to temporary file: %{PUBLIC}@", log: .default, type: .info, tempFileURL.absoluteString) + self.attachFile(localURL: tempFileURL, mimeType: mimeType, completion: completion) + return + } catch { + os_log("Failed to write image data to file: %{PUBLIC}@", log: .default, type: .error, error.localizedDescription) + } + } else { + os_log("Failed to decode base64 string to Data", log: .default, type: .error) + } + } + } + } catch { + os_log("Failed to parse data: %{PUBLIC}@", log: .default, type: .error, error.localizedDescription) } - - completion(attachment) - } catch { - os_log("Failed to create UNNotificationAttachment: %{PUBLIC}@", log: .default, type: .error, error.localizedDescription) completion(nil) } } - task.resume() + } + + func attachFile(localURL: URL, mimeType: String?, completion: @escaping (UNNotificationAttachment?) -> Void) { + do { + let fileManager = FileManager.default + let tempDirectory = NSTemporaryDirectory() + let tempFile = URL(fileURLWithPath: tempDirectory).appendingPathComponent(UUID().uuidString) + + try fileManager.moveItem(at: localURL, to: tempFile) + let attachment: UNNotificationAttachment? + + if let mimeType, + let utType = UTType(mimeType: mimeType), + utType.conforms(to: .data) + { + let newTempFile = tempFile.appendingPathExtension(utType.preferredFilenameExtension ?? "") + try fileManager.moveItem(at: tempFile, to: newTempFile) + attachment = try UNNotificationAttachment(identifier: UUID().uuidString, url: newTempFile, options: nil) + } else { + os_log("Unrecognized MIME type or file extension", log: .default, type: .error) + attachment = nil + } + + completion(attachment) + } catch { + os_log("Failed to create UNNotificationAttachment: %{PUBLIC}@", log: .default, type: .error, error.localizedDescription) + completion(nil) + } } } diff --git a/openHAB.xcodeproj/project.pbxproj b/openHAB.xcodeproj/project.pbxproj index 4f55b54fd..b61307650 100644 --- a/openHAB.xcodeproj/project.pbxproj +++ b/openHAB.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 656916D91FCB82BC00667B2A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 656916D81FCB82BC00667B2A /* GoogleService-Info.plist */; }; 657144512C1E438700C8A1F3 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657144502C1E438700C8A1F3 /* NotificationService.swift */; }; 657144552C1E438700C8A1F3 /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6571444E2C1E438700C8A1F3 /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 657144962C30A16700C8A1F3 /* OpenHABCore in Frameworks */ = {isa = PBXBuildFile; productRef = 657144952C30A16700C8A1F3 /* OpenHABCore */; }; 6595667E28E0BE8E00E8A53B /* MulticastDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6595667D28E0BE8E00E8A53B /* MulticastDelegate.swift */; }; 932602EE2382892B00EAD685 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DAC6608B236F6F4200F4501E /* Assets.xcassets */; }; 933D7F0722E7015100621A03 /* OpenHABUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 933D7F0622E7015000621A03 /* OpenHABUITests.swift */; }; @@ -280,6 +281,7 @@ 6571444E2C1E438700C8A1F3 /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 657144502C1E438700C8A1F3 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 657144522C1E438700C8A1F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 657144972C30A3E300C8A1F3 /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = ""; }; 6595667D28E0BE8E00E8A53B /* MulticastDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MulticastDelegate.swift; sourceTree = ""; }; 931384B324F259BC00A73AB5 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; 931384B424F259BD00A73AB5 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -500,6 +502,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 657144962C30A16700C8A1F3 /* OpenHABCore in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -601,6 +604,7 @@ 6571444F2C1E438700C8A1F3 /* NotificationService */ = { isa = PBXGroup; children = ( + 657144972C30A3E300C8A1F3 /* NotificationService.entitlements */, 657144502C1E438700C8A1F3 /* NotificationService.swift */, 657144522C1E438700C8A1F3 /* Info.plist */, ); @@ -1050,6 +1054,9 @@ dependencies = ( ); name = NotificationService; + packageProductDependencies = ( + 657144952C30A16700C8A1F3 /* OpenHABCore */, + ); productName = NotificationService; productReference = 6571444E2C1E438700C8A1F3 /* NotificationService.appex */; productType = "com.apple.product-type.app-extension"; @@ -1695,6 +1702,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -1740,6 +1748,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -2398,6 +2407,10 @@ package = 93F8063327AE6C620035A6B0 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = FirebaseMessaging; }; + 657144952C30A16700C8A1F3 /* OpenHABCore */ = { + isa = XCSwiftPackageProductDependency; + productName = OpenHABCore; + }; 934E592428F16EBA00162004 /* OpenHABCore */ = { isa = XCSwiftPackageProductDependency; productName = OpenHABCore; diff --git a/openHABWatch Extension/openHABWatch Extension.entitlements b/openHABWatch Extension/openHABWatch Extension.entitlements index 0c67376eb..b6063e1b2 100644 --- a/openHABWatch Extension/openHABWatch Extension.entitlements +++ b/openHABWatch Extension/openHABWatch Extension.entitlements @@ -1,5 +1,10 @@ - + + com.apple.security.application-groups + + group.org.openhab.app + +