Refactor how notifications are preprocessed and be explicit about which ones are supposed to be displayed or discarded.

This commit is contained in:
Stefan Ceriu
2025-02-10 09:57:24 +02:00
committed by Stefan Ceriu
parent 90b4c84ec5
commit 34d8adcf09
+88 -45
View File
@@ -58,6 +58,12 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
} }
} }
deinit {
cleanUp()
ExtensionLogger.logMemory(with: tag)
MXLog.info("\(tag) deinit")
}
override func didReceive(_ request: UNNotificationRequest, override func didReceive(_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
guard !DataProtectionManager.isDeviceLockedAfterReboot(containerURL: URL.appGroupContainerDirectory), guard !DataProtectionManager.isDeviceLockedAfterReboot(containerURL: URL.appGroupContainerDirectory),
@@ -124,6 +130,8 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
MXLog.warning("\(tag) serviceExtensionTimeWillExpire") MXLog.warning("\(tag) serviceExtensionTimeWillExpire")
notify(unreadCount: nil) notify(unreadCount: nil)
} }
// MARK: - Private
private func run(with credentials: KeychainCredentials, private func run(with credentials: KeychainCredentials,
roomID: String, roomID: String,
@@ -141,13 +149,12 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
MXLog.info("\(tag) no notification for the event, discard") MXLog.info("\(tag) no notification for the event, discard")
return discard(unreadCount: unreadCount) return discard(unreadCount: unreadCount)
} }
guard await shouldHandleCallNotification(itemProxy) else { switch await preprocessNotification(itemProxy) {
return discard(unreadCount: unreadCount) case .processedShouldDiscard, .unsupportedShouldDiscard:
}
if await handleRedactionNotification(itemProxy) {
return discard(unreadCount: unreadCount) return discard(unreadCount: unreadCount)
case .shouldDisplay:
break
} }
// After the first processing, update the modified content // After the first processing, update the modified content
@@ -213,14 +220,71 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
handler = nil handler = nil
modifiedContent = nil modifiedContent = nil
} }
deinit { private func preprocessNotification(_ itemProxy: NotificationItemProxyProtocol) async -> NotificationProcessingResult {
cleanUp() guard case let .timeline(event) = itemProxy.event else {
ExtensionLogger.logMemory(with: tag) return .shouldDisplay
MXLog.info("\(tag) deinit") }
switch try? event.eventType() {
case .messageLike(let content):
switch content {
case .poll,
.roomEncrypted,
.sticker:
return .shouldDisplay
case .roomMessage(let messageType, _):
switch messageType {
case .emote, .image, .audio, .video, .file, .notice, .text, .location:
return .shouldDisplay
case .other:
return .unsupportedShouldDiscard
}
case .roomRedaction(let redactedEventID, _):
guard let redactedEventID else {
MXLog.error("Unable to handle redact notification due to missing event ID.")
return .processedShouldDiscard
}
let deliveredNotifications = await UNUserNotificationCenter.current().deliveredNotifications()
if let targetNotification = deliveredNotifications.first(where: { $0.request.content.eventID == redactedEventID }) {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [targetNotification.request.identifier])
}
return .processedShouldDiscard
case .callNotify(let notifyType):
return await handleCallNotification(notifyType: notifyType,
timestamp: event.timestamp(),
roomID: itemProxy.roomID,
roomDisplayName: itemProxy.roomDisplayName)
case .callAnswer,
.callInvite,
.callHangup,
.callCandidates,
.keyVerificationReady,
.keyVerificationStart,
.keyVerificationCancel,
.keyVerificationAccept,
.keyVerificationKey,
.keyVerificationMac,
.keyVerificationDone,
.reactionContent:
return .unsupportedShouldDiscard
}
case .state:
return .unsupportedShouldDiscard
case .none:
return .unsupportedShouldDiscard
}
} }
private func shouldHandleCallNotification(_ itemProxy: NotificationItemProxyProtocol) async -> Bool { /// Handle incoming call notifications.
/// - Returns: A boolean indicating whether the notification was handled and should now be discarded.
private func handleCallNotification(notifyType: NotifyType,
timestamp: Timestamp,
roomID: String,
roomDisplayName: String) async -> NotificationProcessingResult {
// Handle incoming VoIP calls, show the native OS call screen // Handle incoming VoIP calls, show the native OS call screen
// https://developer.apple.com/documentation/callkit/sending-end-to-end-encrypted-voip-calls // https://developer.apple.com/documentation/callkit/sending-end-to-end-encrypted-voip-calls
// //
@@ -233,54 +297,33 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
// - the main app picks this up in `PKPushRegistry.didReceiveIncomingPushWith` and // - the main app picks this up in `PKPushRegistry.didReceiveIncomingPushWith` and
// `CXProvider.reportNewIncomingCall` to show the system UI and handle actions on it. // `CXProvider.reportNewIncomingCall` to show the system UI and handle actions on it.
// N.B. this flow works properly only when background processing capabilities are enabled // N.B. this flow works properly only when background processing capabilities are enabled
guard notifyType == .ring else {
guard case let .timeline(event) = itemProxy.event, return .shouldDisplay
case let .messageLike(content) = try? event.eventType(),
case let .callNotify(notificationType) = content,
notificationType == .ring else {
return true
} }
let timestamp = Date(timeIntervalSince1970: TimeInterval(event.timestamp() / 1000)) let timestamp = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
guard abs(timestamp.timeIntervalSinceNow) < ElementCallServiceNotificationDiscardDelta else { guard abs(timestamp.timeIntervalSinceNow) < ElementCallServiceNotificationDiscardDelta else {
MXLog.info("Call notification is too old, handling as push notification") MXLog.info("Call notification is too old, handling as push notification")
return true return .shouldDisplay
} }
let payload = [ElementCallServiceNotificationKey.roomID.rawValue: itemProxy.roomID, let payload = [ElementCallServiceNotificationKey.roomID.rawValue: roomID,
ElementCallServiceNotificationKey.roomDisplayName.rawValue: itemProxy.roomDisplayName] ElementCallServiceNotificationKey.roomDisplayName.rawValue: roomDisplayName]
do { do {
try await CXProvider.reportNewIncomingVoIPPushPayload(payload) try await CXProvider.reportNewIncomingVoIPPushPayload(payload)
} catch { } catch {
MXLog.error("Failed reporting voip call with error: \(error). Handling as push notification") MXLog.error("Failed reporting voip call with error: \(error). Handling as push notification")
return true return .shouldDisplay
} }
return false return .processedShouldDiscard
} }
/// Handles a notification for an `m.room.redaction` event. private enum NotificationProcessingResult {
/// - Returns: A boolean indicating whether the notification was handled. case shouldDisplay
private func handleRedactionNotification(_ itemProxy: NotificationItemProxyProtocol) async -> Bool { case processedShouldDiscard
guard case let .timeline(event) = itemProxy.event, case unsupportedShouldDiscard
case let .messageLike(content) = try? event.eventType(),
case let .roomRedaction(redactedEventID, _) = content else {
return false
}
guard let redactedEventID else {
MXLog.error("Unable to redact notification due to missing event ID.")
return true // Return true as there's no point showing this notification.
}
let deliveredNotifications = await UNUserNotificationCenter.current().deliveredNotifications()
if let targetNotification = deliveredNotifications.first(where: { $0.request.content.eventID == redactedEventID }) {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [targetNotification.request.identifier])
}
return true
} }
} }