Files
iOS/Sources/Extensions/NotificationService/NotificationService.swift
Zac West 4d9a530637 Reorganize files in repo, pull out build settings from pbxproj (#1140)
This is somewhat in prep of being able to make the project file generated, but also just organizes things into more concrete directory structures.

This pulls out _all_ of the build settings from the root level, and most from the target level, into xcconfigs.

The new directory structure looks like:

- Sources
  - App
    - (everything from HomeAssistant/)
  - WatchApp
  - Shared
  - MacBridge
  - Extensions
    - Intents
    - NotificationContent
    - NotificationService
    - Share
    - Today
    - Watch
    - Widgets
- Tests
  - App
  - UI
  - Shared

Somewhat intentionally, the file structure under these is not yet standardized/organized.

The project targets are now:

- App
- WatchApp
- Shared-iOS
- Shared-watchOS
- MacBridge
- Tests-App
- Tests-UI
- Tests-Shared
- Extension-Intents
- Extension-NotificationContent
- Extension-NotificationService
- Extension-Share
- Extension-Today
- Extension-Widget
- WatchExtension-Watch

This does not yet clean up resources vs. sources, nor does it handle some of the "it's in Sources/App but it's part of Shared" crossover directory issues.
2020-10-03 00:15:04 -07:00

187 lines
7.4 KiB
Swift

//
// NotificationService.swift
// APNSAttachmentService
//
// Created by Robbie Trencheny on 9/8/16.
// Copyright © 2016 Robbie Trencheny. All rights reserved.
//
import UserNotifications
import MobileCoreServices
import Shared
import Alamofire
import PromiseKit
final class NotificationService: UNNotificationServiceExtension {
private var contentHandler: ((UNNotificationContent) -> Void)?
private var bestAttemptContent: UNMutableNotificationContent?
// swiftlint:disable cyclomatic_complexity function_body_length
override func didReceive(_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
Current.Log.verbose("APNSAttachmentService started!")
Current.Log.verbose("Received userInfo \(request.content.userInfo)")
// FIXME: Memory leak caused by ClientEvent/Realm.
/* let event = ClientEvent(text: request.content.clientEventTitle, type: .notification,
payload: request.content.userInfo as? [String: Any])
Current.clientEventStore.addEvent(event) */
Current.Log.debug("Added client event")
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
Current.Log.debug("Set bestAttemptContent")
func failEarly(_ reason: String) {
Current.Log.error("Failing early because \(reason)!")
contentHandler(request.content)
}
guard let content = (request.content.mutableCopy() as? UNMutableNotificationContent) else {
return failEarly("Unable to get mutable copy of notification content")
}
// if there's no attachment, we want to still allow the camera category to be automatic
var incomingAttachment = content.userInfo["attachment"] as? [String: Any] ?? [:]
var needsAuth = false
if content.categoryIdentifier.lowercased().hasPrefix("camera") && incomingAttachment["url"] == nil {
Current.Log.debug("Camera cat prefix")
guard let entityId = content.userInfo["entity_id"] as? String else {
return failEarly("Category identifier was prefixed camera but no entity_id was set")
}
incomingAttachment["url"] = "/api/camera_proxy/\(entityId)"
if incomingAttachment["content-type"] == nil {
incomingAttachment["content-type"] = "jpeg"
}
needsAuth = true
Current.Log.debug("Camera so requiring auth")
} else {
Current.Log.debug("Not a camera notification")
// Check if we still have an empty dictionary
if incomingAttachment.isEmpty {
// Attachment wasn't there/not a string:any, and this isn't a camera category, so we should fail
return failEarly("Content dictionary was not empty")
}
}
guard let attachmentString = incomingAttachment["url"] as? String else {
return failEarly("url string did not exist in dictionary")
}
if attachmentString.hasPrefix("/") { // URL is something like /api or /www so lets prepend base URL
Current.Log.debug("Appears to be local URL, requiring auth")
needsAuth = true
}
guard let attachmentURL = URL(string: attachmentString) else {
return failEarly("Could not convert string to URL")
}
var attachmentOptions: [String: Any] = [:]
if let attachmentContentType = incomingAttachment["content-type"] as? String {
attachmentOptions[UNNotificationAttachmentOptionsTypeHintKey] =
self.contentTypeForString(attachmentContentType)
}
if let attachmentHideThumbnail = incomingAttachment["hide-thumbnail"] as? Bool {
attachmentOptions[UNNotificationAttachmentOptionsThumbnailHiddenKey] = attachmentHideThumbnail
}
Current.Log.debug("Set attachment options to \(attachmentOptions)s")
Current.Log.verbose("Going to get URL at \(attachmentURL)")
firstly {
return HomeAssistantAPI.authenticatedAPIPromise
}.then { api in
return api.DownloadDataAt(url: attachmentURL, needsAuth: needsAuth)
}.done { fileURL in
do {
let attachment = try UNNotificationAttachment(identifier: attachmentURL.lastPathComponent, url: fileURL,
options: attachmentOptions)
content.attachments.append(attachment)
} catch let error {
return failEarly("Unable to build UNNotificationAttachment: \(error)")
}
Current.Log.debug("Successfully created and appended attachment \(content.attachments)")
// Attempt to fill in the summary argument with the thread or category ID if it doesn't exist in payload.
if #available(iOS 12.0, *) {
if content.summaryArgument == "" {
if content.threadIdentifier != "" {
content.summaryArgument = content.threadIdentifier
} else if content.categoryIdentifier != "" {
content.summaryArgument = content.categoryIdentifier
}
}
}
Current.Log.debug("About to return")
guard let copiedContent = content.copy() as? UNNotificationContent else {
return failEarly("Unable to copy contents")
}
Current.Log.debug("Returning \(copiedContent)")
contentHandler(copiedContent)
}.catch { error in
var reason = "Error when getting attachment data! \(error)"
if let error = error as? AFError {
reason = "Alamofire error while getting attachment data: \(error)"
}
return failEarly(reason)
}
}
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.
Current.Log.warning("serviceExtensionTimeWillExpire")
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
private func contentTypeForString(_ contentTypeString: String) -> CFString {
let contentType: CFString
switch contentTypeString.lowercased() {
case "aiff":
contentType = kUTTypeAudioInterchangeFileFormat
case "avi":
contentType = kUTTypeAVIMovie
case "gif":
contentType = kUTTypeGIF
case "jpeg", "jpg":
contentType = kUTTypeJPEG
case "mp3":
contentType = kUTTypeMP3
case "mpeg":
contentType = kUTTypeMPEG
case "mpeg2":
contentType = kUTTypeMPEG2Video
case "mpeg4":
contentType = kUTTypeMPEG4
case "mpeg4audio":
contentType = kUTTypeMPEG4Audio
case "png":
contentType = kUTTypePNG
case "waveformaudio":
contentType = kUTTypeWaveformAudio
default:
contentType = contentTypeString as CFString
}
return contentType
}
}