Skip to content

Commit

Permalink
Allows for the use of "item:imageItem" for attachments
Browse files Browse the repository at this point in the history
Signed-off-by: Dan Cunningham <[email protected]>
  • Loading branch information
digitaldan committed Jun 29, 2024
1 parent 054b91f commit 53b55ae
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 41 deletions.
10 changes: 10 additions & 0 deletions NotificationService/NotificationService.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.openhab.app</string>
</array>
</dict>
</plist>
149 changes: 109 additions & 40 deletions NotificationService/NotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,30 @@
//
// SPDX-License-Identifier: EPL-2.0

import Foundation
import OpenHABCore
import os.log
import UniformTypeIdentifiers
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)
if let bestAttemptContent {
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") {
Expand All @@ -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) {
Expand All @@ -64,27 +67,37 @@ 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 {
contentHandler(bestAttemptContent)
}
}
}

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.
Expand All @@ -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) {
Expand All @@ -107,50 +120,106 @@ 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 {
os_log("Error downloading media %{PUBLIC}@", log: .default, type: .error, error?.localizedDescription ?? "Unknown error")
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)
}
}
}
13 changes: 13 additions & 0 deletions openHAB.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 = "<group>"; };
657144522C1E438700C8A1F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
657144972C30A3E300C8A1F3 /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = "<group>"; };
6595667D28E0BE8E00E8A53B /* MulticastDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MulticastDelegate.swift; sourceTree = "<group>"; };
931384B324F259BC00A73AB5 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
931384B424F259BD00A73AB5 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -500,6 +502,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
657144962C30A16700C8A1F3 /* OpenHABCore in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -601,6 +604,7 @@
6571444F2C1E438700C8A1F3 /* NotificationService */ = {
isa = PBXGroup;
children = (
657144972C30A3E300C8A1F3 /* NotificationService.entitlements */,
657144502C1E438700C8A1F3 /* NotificationService.swift */,
657144522C1E438700C8A1F3 /* Info.plist */,
);
Expand Down Expand Up @@ -1050,6 +1054,9 @@
dependencies = (
);
name = NotificationService;
packageProductDependencies = (
657144952C30A16700C8A1F3 /* OpenHABCore */,
);
productName = NotificationService;
productReference = 6571444E2C1E438700C8A1F3 /* NotificationService.appex */;
productType = "com.apple.product-type.app-extension";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -2398,6 +2407,10 @@
package = 93F8063327AE6C620035A6B0 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseMessaging;
};
657144952C30A16700C8A1F3 /* OpenHABCore */ = {
isa = XCSwiftPackageProductDependency;
productName = OpenHABCore;
};
934E592428F16EBA00162004 /* OpenHABCore */ = {
isa = XCSwiftPackageProductDependency;
productName = OpenHABCore;
Expand Down
7 changes: 6 additions & 1 deletion openHABWatch Extension/openHABWatch Extension.entitlements
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.openhab.app</string>
</array>
</dict>
</plist>

0 comments on commit 53b55ae

Please sign in to comment.