mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-18 02:13:08 +09:00
274 lines
11 KiB
Swift
274 lines
11 KiB
Swift
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
import Shared
|
|
import Storage
|
|
import Sync
|
|
import UserNotifications
|
|
|
|
class NotificationService: UNNotificationServiceExtension {
|
|
var display: SyncDataDisplay?
|
|
var profile: ExtensionProfile?
|
|
|
|
// This is run when an APNS notification with `mutable-content` is received.
|
|
// If the app is backgrounded, then the alert notification is displayed.
|
|
// If the app is foregrounded, then the notification.userInfo is passed straight to
|
|
// AppDelegate.application(_:didReceiveRemoteNotification:completionHandler:)
|
|
// Once the notification is tapped, then the same userInfo is passed to the same method in the AppDelegate.
|
|
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
|
let userInfo = request.content.userInfo
|
|
|
|
guard let content = (request.content.mutableCopy() as? UNMutableNotificationContent) else {
|
|
return self.didFinish(PushMessage.accountVerified)
|
|
}
|
|
|
|
if self.profile == nil {
|
|
self.profile = ExtensionProfile(localName: "profile")
|
|
}
|
|
|
|
guard let profile = self.profile else {
|
|
self.didFinish(with: .noProfile)
|
|
return
|
|
}
|
|
|
|
let queue = profile.queue
|
|
let display = SyncDataDisplay(content: content, contentHandler: contentHandler, tabQueue: queue)
|
|
self.display = display
|
|
profile.syncDelegate = display
|
|
|
|
let handler = FxAPushMessageHandler(with: profile)
|
|
|
|
handler.handle(userInfo: userInfo).upon { res in
|
|
self.didFinish(res.successValue, with: res.failureValue as? PushMessageError)
|
|
}
|
|
}
|
|
|
|
func didFinish(_ what: PushMessage? = nil, with error: PushMessageError? = nil) {
|
|
profile?.shutdown()
|
|
|
|
guard let display = self.display else {
|
|
return
|
|
}
|
|
|
|
// We cannot use tabqueue after the profile has shutdown;
|
|
// however, we can't use weak references, because TabQueue isn't a class.
|
|
// Rather than changing tabQueue, we manually nil it out here.
|
|
display.tabQueue = nil
|
|
display.messageDelivered = false
|
|
display.displayNotification(what, with: error)
|
|
if !display.messageDelivered {
|
|
display.displayUnknownMessageNotification()
|
|
}
|
|
}
|
|
|
|
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.
|
|
didFinish(with: .timeout)
|
|
}
|
|
}
|
|
|
|
class SyncDataDisplay {
|
|
var contentHandler: ((UNNotificationContent) -> Void)
|
|
var notificationContent: UNMutableNotificationContent
|
|
var sentTabs: [SentTab]
|
|
|
|
var tabQueue: TabQueue?
|
|
var messageDelivered: Bool = false
|
|
|
|
init(content: UNMutableNotificationContent, contentHandler: @escaping (UNNotificationContent) -> Void, tabQueue: TabQueue) {
|
|
self.contentHandler = contentHandler
|
|
self.notificationContent = content
|
|
self.sentTabs = []
|
|
self.tabQueue = tabQueue
|
|
}
|
|
|
|
func displayNotification(_ message: PushMessage? = nil, with error: PushMessageError? = nil) {
|
|
guard let message = message, error == nil else {
|
|
return displayUnknownMessageNotification()
|
|
}
|
|
|
|
switch message {
|
|
case .accountVerified:
|
|
displayAccountVerifiedNotification()
|
|
case .deviceConnected(let deviceName):
|
|
displayDeviceConnectedNotification(deviceName)
|
|
case .deviceDisconnected(let deviceName):
|
|
displayDeviceDisconnectedNotification(deviceName)
|
|
case .thisDeviceDisconnected:
|
|
displayThisDeviceDisconnectedNotification()
|
|
case .collectionChanged(let collections):
|
|
if collections.contains("clients") {
|
|
displaySentTabNotification()
|
|
} else {
|
|
displayUnknownMessageNotification()
|
|
}
|
|
default:
|
|
displayUnknownMessageNotification()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SyncDataDisplay {
|
|
func displayDeviceConnectedNotification(_ deviceName: String) {
|
|
presentNotification(title: Strings.FxAPush_DeviceConnected_title,
|
|
body: Strings.FxAPush_DeviceConnected_body,
|
|
bodyArg: deviceName)
|
|
}
|
|
|
|
func displayDeviceDisconnectedNotification(_ deviceName: String?) {
|
|
if let deviceName = deviceName {
|
|
presentNotification(title: Strings.FxAPush_DeviceDisconnected_title,
|
|
body: Strings.FxAPush_DeviceDisconnected_body,
|
|
bodyArg: deviceName)
|
|
} else {
|
|
// We should never see this branch
|
|
presentNotification(title: Strings.FxAPush_DeviceDisconnected_title,
|
|
body: Strings.FxAPush_DeviceDisconnected_UnknownDevice_body)
|
|
}
|
|
}
|
|
|
|
func displayThisDeviceDisconnectedNotification() {
|
|
presentNotification(title: Strings.FxAPush_DeviceDisconnected_ThisDevice_title,
|
|
body: Strings.FxAPush_DeviceDisconnected_ThisDevice_body)
|
|
}
|
|
|
|
func displayAccountVerifiedNotification() {
|
|
presentNotification(title: Strings.SentTab_NoTabArrivingNotification_title, body: Strings.SentTab_NoTabArrivingNotification_body)
|
|
}
|
|
|
|
func displayUnknownMessageNotification() {
|
|
// if, by any change we haven't dealt with the message, then perhaps we
|
|
// can recycle it as a sent tab message.
|
|
if sentTabs.count > 0 {
|
|
displaySentTabNotification()
|
|
} else {
|
|
presentNotification(title: Strings.SentTab_NoTabArrivingNotification_title, body: Strings.SentTab_NoTabArrivingNotification_body)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SyncDataDisplay {
|
|
func displaySentTabNotification() {
|
|
// We will need to be more precise about calling these SentTab alerts
|
|
// once we are a) detecting different types of notifications and b) adding actions.
|
|
// For now, we need to add them so we can handle zero-tab sent-tab-notifications.
|
|
notificationContent.categoryIdentifier = "org.mozilla.ios.SentTab.placeholder"
|
|
|
|
var userInfo = notificationContent.userInfo
|
|
|
|
// Add the tabs we've found to userInfo, so that the AppDelegate
|
|
// doesn't have to do it again.
|
|
let serializedTabs = sentTabs.flatMap { t -> NSDictionary? in
|
|
return [
|
|
"title": t.title,
|
|
"url": t.url.absoluteString,
|
|
"displayURL": t.url.absoluteDisplayExternalString,
|
|
"deviceName": t.deviceName as Any,
|
|
] as NSDictionary
|
|
}
|
|
|
|
func present(_ tabs: [NSDictionary]) {
|
|
if !tabs.isEmpty {
|
|
userInfo["sentTabs"] = tabs as NSArray
|
|
}
|
|
notificationContent.userInfo = userInfo
|
|
presentSentTabsNotification(tabs)
|
|
}
|
|
|
|
let center = UNUserNotificationCenter.current()
|
|
center.getDeliveredNotifications { notifications in
|
|
// Let's deal with sent-tab-notifications
|
|
let sentTabNotifications = notifications.filter {
|
|
$0.request.content.categoryIdentifier == self.notificationContent.categoryIdentifier
|
|
}
|
|
|
|
// We can delete zero tab sent-tab-notifications
|
|
let emptyTabNotificationsIds = sentTabNotifications.filter {
|
|
$0.request.content.userInfo["sentTabs"] == nil
|
|
}.map { $0.request.identifier }
|
|
center.removeDeliveredNotifications(withIdentifiers: emptyTabNotificationsIds)
|
|
|
|
// The one we've just received (but not delivered) may not have any tabs in it either
|
|
// e.g. if the previous one consumed two tabs.
|
|
if serializedTabs.count == 0 {
|
|
// In that case, we try and recycle an existing notification (one that has a tab in it).
|
|
if let firstNonEmpty = sentTabNotifications.first(where: { $0.request.content.userInfo["sentTabs"] != nil }),
|
|
let previouslyDeliveredTabs = firstNonEmpty.request.content.userInfo["sentTabs"] as? [NSDictionary] {
|
|
center.removeDeliveredNotifications(withIdentifiers: [firstNonEmpty.request.identifier])
|
|
return present(previouslyDeliveredTabs)
|
|
}
|
|
}
|
|
|
|
// We have tabs in this notification, or we couldn't recycle an existing one that does.
|
|
present(serializedTabs)
|
|
}
|
|
}
|
|
|
|
func presentSentTabsNotification(_ tabs: [NSDictionary]) {
|
|
let title: String
|
|
let body: String
|
|
|
|
if tabs.count == 0 {
|
|
title = Strings.SentTab_NoTabArrivingNotification_title
|
|
body = Strings.SentTab_NoTabArrivingNotification_body
|
|
} else {
|
|
let deviceNames = Set(tabs.flatMap { $0["deviceName"] as? String })
|
|
if let deviceName = deviceNames.first, deviceNames.count == 1 {
|
|
title = String(format: Strings.SentTab_TabArrivingNotification_WithDevice_title, deviceName)
|
|
} else {
|
|
title = Strings.SentTab_TabArrivingNotification_NoDevice_title
|
|
}
|
|
|
|
if tabs.count == 1 {
|
|
// We give the fallback string as the url,
|
|
// because we have only just introduced "displayURL" as a key.
|
|
body = (tabs[0]["displayURL"] as? String) ??
|
|
(tabs[0]["url"] as! String)
|
|
} else if deviceNames.count == 0 {
|
|
body = Strings.SentTab_TabArrivingNotification_NoDevice_body
|
|
} else {
|
|
body = String(format: Strings.SentTab_TabArrivingNotification_WithDevice_body, AppInfo.displayName)
|
|
}
|
|
}
|
|
|
|
presentNotification(title: title, body: body)
|
|
}
|
|
|
|
func presentNotification(title: String, body: String, titleArg: String? = nil, bodyArg: String? = nil) {
|
|
func stringWithOptionalArg(_ s: String, _ a: String?) -> String {
|
|
if let a = a {
|
|
return String(format: s, a)
|
|
}
|
|
return s
|
|
}
|
|
|
|
notificationContent.title = stringWithOptionalArg(title, titleArg)
|
|
notificationContent.body = stringWithOptionalArg(body, bodyArg)
|
|
|
|
// This is the only place we call the contentHandler.
|
|
contentHandler(notificationContent)
|
|
// This is the only place we change messageDelivered. We can check if contentHandler hasn't be called because of
|
|
// our logic (rather than something funny with our environment, or iOS killing us).
|
|
messageDelivered = true
|
|
}
|
|
}
|
|
|
|
extension SyncDataDisplay: SyncDelegate {
|
|
func displaySentTab(for url: URL, title: String, from deviceName: String?) {
|
|
if url.isWebPage() {
|
|
sentTabs.append(SentTab(url: url, title: title, deviceName: deviceName))
|
|
|
|
let item = ShareItem(url: url.absoluteString, title: title, favicon: nil)
|
|
_ = tabQueue?.addToQueue(item)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SentTab {
|
|
let url: URL
|
|
let title: String
|
|
let deviceName: String?
|
|
}
|