element-x-ios/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift

205 lines
8.9 KiB
Swift

//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import Foundation
import UIKit
import UserNotifications
class NotificationManager: NSObject, NotificationManagerProtocol {
private let notificationCenter: UserNotificationCenterProtocol
private var userSession: UserSessionProtocol?
var clientCancellable: AnyCancellable?
init(notificationCenter: UserNotificationCenterProtocol = UNUserNotificationCenter.current()) {
self.notificationCenter = notificationCenter
super.init()
}
// MARK: NotificationManagerProtocol
weak var delegate: NotificationManagerDelegate?
func start() {
let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply,
title: L10n.actionQuickReply,
options: [])
let replyCategory = UNNotificationCategory(identifier: NotificationConstants.Category.reply,
actions: [replyAction],
intentIdentifiers: [],
options: [])
notificationCenter.setNotificationCategories([replyCategory])
notificationCenter.delegate = self
}
func requestAuthorization() {
Task {
do {
let granted = try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge])
MXLog.info("[NotificationManager] permission granted: \(granted)")
await MainActor.run {
self.delegate?.authorizationStatusUpdated(self, granted: granted)
}
} catch {
MXLog.error("[NotificationManager] request authorization failed: \(error)")
}
}
}
func register(with deviceToken: Data) async -> Bool {
guard let userSession else {
return false
}
return await setPusher(with: deviceToken, clientProxy: userSession.clientProxy)
}
func setUserSession(_ userSession: UserSessionProtocol?) {
self.userSession = userSession
clientCancellable = userSession?.clientProxy.callbacks.sink { [weak self] value in
guard let self else { return }
switch value {
case let .receivedNotification(notification):
Task {
await self.showLocalNotification(notification)
}
default:
return
}
}
}
func registrationFailed(with error: Error) { }
func showLocalNotification(with title: String, subtitle: String?) async {
let content = UNMutableNotificationContent()
content.title = title
if let subtitle {
content.subtitle = subtitle
}
let request = UNNotificationRequest(identifier: ProcessInfo.processInfo.globallyUniqueString,
content: content,
trigger: nil)
do {
try await notificationCenter.add(request)
MXLog.info("[NotificationManager] show local notification succeeded")
} catch {
MXLog.error("[NotificationManager] show local notification failed: \(error)")
}
}
private func showLocalNotification(_ notification: NotificationItemProxyProtocol) async {
guard let userSession,
notification.event.timestamp > ServiceLocator.shared.settings.lastLaunchDate else { return }
do {
guard let identifier = notification.id else {
return
}
let content = try await notification.process(mediaProvider: userSession.mediaProvider)
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
guard !ServiceLocator.shared.settings.servedNotificationIdentifiers.contains(identifier) else {
MXLog.info("NotificationManager] local notification discarded because it has already been served")
return
}
ServiceLocator.shared.settings.servedNotificationIdentifiers.insert(identifier)
try await notificationCenter.add(request)
} catch {
MXLog.error("[NotificationManager] show local notification item failed: \(error)")
}
}
private func setPusher(with deviceToken: Data, clientProxy: ClientProxyProtocol) async -> Bool {
do {
let defaultPayload = APNSPayload(aps: APSInfo(mutableContent: 1,
alert: APSAlert(locKey: "Notification",
locArgs: [])),
pusherNotificationClientIdentifier: clientProxy.restorationToken?.pusherNotificationClientIdentifier)
let configuration = try await PusherConfiguration(identifiers: .init(pushkey: deviceToken.base64EncodedString(),
appId: ServiceLocator.shared.settings.pusherAppId),
kind: .http(data: .init(url: ServiceLocator.shared.settings.pushGatewayBaseURL.absoluteString,
format: .eventIdOnly,
defaultPayload: defaultPayload.toJsonString())),
appDisplayName: "\(InfoPlistReader.main.bundleDisplayName) (iOS)",
deviceDisplayName: UIDevice.current.name,
profileTag: pusherProfileTag(),
lang: Bundle.app.preferredLocalizations.first ?? "en")
try await clientProxy.setPusher(with: configuration)
MXLog.info("[NotificationManager] set pusher succeeded")
return true
} catch {
MXLog.error("[NotificationManager] set pusher failed: \(error)")
return false
}
}
private func pusherProfileTag() -> String {
if let currentTag = ServiceLocator.shared.settings.pusherProfileTag {
return currentTag
}
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
let newTag = (0..<16).map { _ in
let offset = Int.random(in: 0..<chars.count)
return String(chars[chars.index(chars.startIndex, offsetBy: offset)])
}.joined()
ServiceLocator.shared.settings.pusherProfileTag = newTag
return newTag
}
}
// MARK: - UNUserNotificationCenterDelegate
extension NotificationManager: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
guard ServiceLocator.shared.settings.enableInAppNotifications else {
return []
}
guard let delegate else {
return [.badge, .sound, .list, .banner]
}
guard delegate.shouldDisplayInAppNotification(self, content: notification.request.content) else {
return []
}
return [.badge, .sound, .list, .banner]
}
@MainActor
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
switch response.actionIdentifier {
case NotificationConstants.Action.inlineReply:
guard let response = response as? UNTextInputNotificationResponse else {
return
}
await delegate?.handleInlineReply(self,
content: response.notification.request.content,
replyText: response.userText)
case UNNotificationDefaultActionIdentifier:
await delegate?.notificationTapped(self,
content: response.notification.request.content)
default:
break
}
}
}
extension UNUserNotificationCenter: UserNotificationCenterProtocol { }