@ -15,11 +15,13 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
private var didPerformSetup = false
private var contentHandler : ( ( UNNotificationContent ) -> Void ) ?
private var request : UNNotificationRequest ?
private var openGroupPollCancellable : AnyCancellable ?
public static let isFromRemoteKey = " remote "
public static let threadIdKey = " Signal.AppNotificationsUserInfoKey.threadId "
public static let threadVariantRaw = " Signal.AppNotificationsUserInfoKey.threadVariantRaw "
public static let threadNotificationCounter = " Session.AppNotificationsUserInfoKey.threadNotificationCounter "
private static let callPreOfferLargeNotificationSupressionDuration : TimeInterval = 30
// MARK: D i d r e c e i v e a r e m o t e p u s h n o t i f i c a t i o n r e q u e s t
@ -43,6 +45,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
let isCallOngoing : Bool = ( UserDefaults . sharedLokiProject ? [ . isCallOngoing ] )
. defaulting ( to : false )
let lastCallPreOffer : Date ? = UserDefaults . sharedLokiProject ? [ . lastCallPreOffer ]
// P e r f o r m m a i n s e t u p
Storage . resumeDatabaseAccess ( )
@ -52,14 +55,13 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
AppReadiness . runNowOrWhenAppDidBecomeReady {
let openGroupPollingPublishers : [ AnyPublisher < Void , Error > ] = self . pollForOpenGroups ( )
defer {
Publishers
self . openGroupPollCancellable = Publishers
. MergeMany ( openGroupPollingPublishers )
. subscribe ( on : DispatchQueue . global ( qos : . background ) )
. subscribe ( on : DispatchQueue . main )
. sinkUntilComplete (
receiveCompletion : { _ in
self . completeSilenty ( )
}
. sink (
receiveCompletion : { [ weak self ] _ in self ? . completeSilenty ( ) } ,
receiveValue : { _ in }
)
}
@ -75,9 +77,21 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// I f w e g o t a n e x p l i c i t f a i l u r e , o r w e g o t a s u c c e s s b u t n o c o n t e n t t h e n s h o w
// t h e f a l l b a c k n o t i f i c a t i o n
case . success , . legacySuccess , . failure , . legacyFailure :
return self . handleFailure ( for : notificationContent )
case . legacyForceSilent : return
return self . handleFailure ( for : notificationContent , error : . processing ( result ) )
case . successTooLong :
// / I f t h e n o t i f i c a t i o n i s t o o l o n g a n d t h e r e i s a n o n g o i n g c a l l o r a r e c e n t c a l l p r e - o f f e r t h e n w e a s s u m e t h e n o t i f i c a t i o n
// / i s a c a l l ` I C E _ C A N D I D A T E S ` m e s s a g e a n d j u s t c o m p l e t e s i l e n t l y ( b e c a u s e t h e f a l l b a c k w o u l d b e a n n o y i n g ) , i f n o t
// / t h e n w e d o w a n t t o s h o w t h e f a l l b a c k n o t i f i c a t i o n
guard
isCallOngoing ||
( lastCallPreOffer ? ? Date . distantPast ) . timeIntervalSinceNow < NotificationServiceExtension . callPreOfferLargeNotificationSupressionDuration
else { return self . handleFailure ( for : notificationContent , error : . processing ( result ) ) }
NSLog ( " [NotificationServiceExtension] Suppressing large notification too close to a call. " )
return
case . legacyForceSilent , . failureNoContent : return
}
}
@ -87,10 +101,26 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
Storage . shared . write { db in
do {
guard let processedMessage : ProcessedMessage = try Message . processRawReceivedMessageAsNotification ( db , envelope : envelope ) else {
self . handleFailure ( for : notificationContent )
self . handleFailure ( for : notificationContent , error : . messageProcessing )
return
}
// / D u e t o t h e w a y t h e ` C a l l M e s s a g e ` a n d ` S h a r e d C o n f i g M e s s a g e ` w o r k w e n e e d t o c u s t o m
// / h a n d l e t h e i r b e h a v i o u r s , f o r a l l o t h e r m e s s a g e t y p e s w e w a n t t o j u s t u s e s t a n d a r d m e s s a g e s
switch processedMessage . messageInfo . message {
case is CallMessage , is SharedConfigMessage : break
default :
try MessageReceiver . handle (
db ,
threadId : processedMessage . threadId ,
threadVariant : processedMessage . threadVariant ,
message : processedMessage . messageInfo . message ,
serverExpirationTimestamp : processedMessage . messageInfo . serverExpirationTimestamp ,
associatedWithProto : processedMessage . proto
)
return
}
// T h r o w i f t h e m e s s a g e i s o u t d a t e d a n d s h o u l d n ' t b e p r o c e s s e d
try MessageReceiver . throwIfMessageOutdated (
db ,
@ -100,47 +130,6 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
)
switch processedMessage . messageInfo . message {
case let visibleMessage as VisibleMessage :
let interactionId : Int64 = try MessageReceiver . handleVisibleMessage (
db ,
threadId : processedMessage . threadId ,
threadVariant : processedMessage . threadVariant ,
message : visibleMessage ,
associatedWithProto : processedMessage . proto
)
// R e m o v e t h e n o t i f i c a t i o n s i f t h e r e i s a n o u t g o i n g m e s s a g e s f r o m a l i n k e d d e v i c e
if
let interaction : Interaction = try ? Interaction . fetchOne ( db , id : interactionId ) ,
interaction . variant = = . standardOutgoing
{
let semaphore = DispatchSemaphore ( value : 0 )
let center = UNUserNotificationCenter . current ( )
center . getDeliveredNotifications { notifications in
let matchingNotifications = notifications . filter ( { $0 . request . content . userInfo [ NotificationServiceExtension . threadIdKey ] as ? String = = interaction . threadId } )
center . removeDeliveredNotifications ( withIdentifiers : matchingNotifications . map ( { $0 . request . identifier } ) )
// H a c k : r e m o v e D e l i v e r e d N o t i f i c a t i o n s s e e m s t o b e a s y n c , n e e d t o w a i t f o r s o m e t i m e b e f o r e t h e d e l i v e r e d n o t i f i c a t i o n s c a n b e r e m o v e d .
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.1 ) { semaphore . signal ( ) }
}
semaphore . wait ( )
}
case let unsendRequest as UnsendRequest :
try MessageReceiver . handleUnsendRequest (
db ,
threadId : processedMessage . threadId ,
threadVariant : processedMessage . threadVariant ,
message : unsendRequest
)
case let closedGroupControlMessage as ClosedGroupControlMessage :
try MessageReceiver . handleClosedGroupControlMessage (
db ,
threadId : processedMessage . threadId ,
threadVariant : processedMessage . threadVariant ,
message : closedGroupControlMessage
)
case let callMessage as CallMessage :
try MessageReceiver . handleCallMessage (
db ,
@ -187,6 +176,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
break
}
try MessageReceiver . insertCallInfoMessage ( db , for : callMessage )
self . handleSuccessForIncomingCall ( db , for : callMessage )
case let sharedConfigMessage as SharedConfigMessage :
@ -210,7 +200,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
if let error = error as ? MessageReceiverError , error . isRetryable {
switch error {
case . invalidGroupPublicKey , . noGroupKeyPair , . outdatedMessage : self . completeSilenty ( )
default : self . handleFailure ( for : notificationContent )
default : self . handleFailure ( for : notificationContent , error : . messageHandling ( error ) )
}
}
}
@ -236,6 +226,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
Cryptography . seedRandom ( )
AppSetup . setupEnvironment (
retrySetupIfDatabaseInvalid : true ,
appSpecificBlock : {
Environment . shared ? . notificationsManager . mutate {
$0 = NSENotificationPresenter ( )
@ -307,14 +298,21 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
override public func serviceExtensionTimeWillExpire ( ) {
// C a l l e d j u s t b e f o r e t h e e x t e n s i o n w i l l b e t e r m i n a t e d b y t h e s y s t e m .
// U s e t h i s a s a n o p p o r t u n i t y t o d e l i v e r y o u r " b e s t a t t e m p t " a t m o d i f i e d c o n t e n t , o t h e r w i s e t h e o r i g i n a l p u s h p a y l o a d w i l l b e u s e d .
NSLog ( " [NotificationServiceExtension] Execution time expired " )
openGroupPollCancellable ? . cancel ( )
completeSilenty ( )
}
private func completeSilenty ( ) {
NSLog ( " [NotificationServiceExtension] Complete silently " )
let silentContent : UNMutableNotificationContent = UNMutableNotificationContent ( )
silentContent . badge = Storage . shared
. read { db in try Interaction . fetchUnreadCount ( db ) }
. map { NSNumber ( value : $0 ) }
. defaulting ( to : NSNumber ( value : 0 ) )
Storage . suspendDatabaseAccess ( )
self . contentHandler ! ( . init ( ) )
self . contentHandler ! ( silentContent )
}
private func handleSuccessForIncomingCall ( _ db : Database , for callMessage : CallMessage ) {
@ -330,11 +328,12 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
CXProvider . reportNewIncomingVoIPPushPayload ( payload ) { error in
if let error = error {
self . handleFailureForVoIP ( db , for : callMessage )
S NLog( " Failed to notify main app of call message: \( error ) " )
NS Log( " [NotificationServiceExtension] Failed to notify main app of call message: \( error ) " )
}
else {
NSLog ( " [NotificationServiceExtension] Successfully notified main app of call message. " )
UserDefaults . sharedLokiProject ? [ . lastCallPreOffer ] = Date ( )
self . completeSilenty ( )
SNLog ( " Successfully notified main app of call message. " )
}
}
}
@ -347,11 +346,9 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
let notificationContent = UNMutableNotificationContent ( )
notificationContent . userInfo = [ NotificationServiceExtension . isFromRemoteKey : true ]
notificationContent . title = " Session "
// B a d g e N u m b e r
let newBadgeNumber = CurrentAppContext ( ) . appUserDefaults ( ) . integer ( forKey : " currentBadgeNumber " ) + 1
notificationContent . badge = NSNumber ( value : newBadgeNumber )
CurrentAppContext ( ) . appUserDefaults ( ) . set ( newBadgeNumber , forKey : " currentBadgeNumber " )
notificationContent . badge = ( try ? Interaction . fetchUnreadCount ( db ) )
. map { NSNumber ( value : $0 ) }
. defaulting ( to : NSNumber ( value : 0 ) )
if let sender : String = callMessage . sender {
let senderDisplayName : String = Profile . displayName ( db , id : sender , threadVariant : . contact )
@ -367,20 +364,21 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
UNUserNotificationCenter . current ( ) . add ( request ) { error in
if let error = error {
S NLog( " Failed to add notification request due to error:\( error ) " )
NS Log( " [NotificationServiceExtension] Failed to add notification request due to error: \( error ) " )
}
semaphore . signal ( )
}
semaphore . wait ( )
S NLog( " Add remote notification request" )
NS Log( " [NotificationServiceExtension] Add remote notification request" )
}
private func handleFailure ( for content : UNMutableNotificationContent ) {
private func handleFailure ( for content : UNMutableNotificationContent , error : NotificationError ) {
NSLog ( " [NotificationServiceExtension] Show generic failure message due to error: \( error ) " )
Storage . suspendDatabaseAccess ( )
content . body = " You've got a new message "
content . title = " Session "
let userInfo : [ String : Any ] = [ NotificationServiceExtension . isFromRemoteKey : true ]
content . body = " APN_Message " . localized ( )
let userInfo : [ String : Any ] = [ NotificationServiceExtension . isFromRemoteKey : true ]
content . userInfo = userInfo
contentHandler ! ( content )
}