IOS-Swiftui background call ring vibration

IOS background ringing, implementation solution through UNNotificationServiceExtension

  1. Xcode creates UNNotificationServiceExtension target
  2. Process the specified notification content through the didReceive method of NotificationService
  3. Use UserDefaults(suiteName: self.groupName) for shared data transfer

Create UNNotificationServiceExtension target

Click Xcode-File-New-Target and the following options will pop up for filtering selections

Here we choose Notification Service Extension

Click Finish here to complete the creation. Xcode will automatically generate the necessary code.

NotificationService is used to intercept notifications. When a notification is received, the didReceive method will be triggered.

To debug and determine whether the notification extension service is effective, you only need to check whether the pop-up notification has added “[modified]” content.

Implementation ideas

Call request is one type of notification, and hanging up is another type of notification. When receiving a call request, there will be up to 30 seconds to activate the application, and we need to continue ringing + vibrating during this time.

Start ringing
When receiving an audio and video call request notification, check whether the APP is in the foreground. When it is in the foreground, the main program will ring and vibrate by itself. Here, we only handle the situation where the APP is not in the foreground and ring and vibrate.
Stop ringing
After the 30-second processing time is up, the system will actively call the serviceExtensionTimeWillExpire timeout method. We can also actively stop the ringing and vibration before the timeout. Here, check whether the notification should be activated every second. If the user opens the main program, you need to modify the callKeyEnterToForeground parameter in the shared memory to true in the main program. In this way, the NotificationService will be able to check that the user APP is opened, and Stop your own ringing and vibration. Whether you need to ring and vibrate later is handled by the main program itself.
When receiving a notification to stop ringing, reset keyStartCallTime to 0. In this way, when the vibration cycle check status is checked, it can also be checked that the hangup notification has been received, thus terminating the ringing vibration logic.

The following is the complete code

In UNNotificationServiceExtension, some data exchange needs to be done with the main program. This is achieved through shared grouping to understand the status of the main program.

import UserNotifications
import AudioToolbox
import AVFAudio
importUIKit

class NotificationService: UNNotificationServiceExtension {<!-- -->

    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?
    
    private let keyStartCallTime = "keyStartCallTime"
    private let groupName = "group.xxp.ring.callNotification"
    private let callKeyEnterToForeground = "enterToForground"
    
    //Set the maximum notification duration to 30 seconds
    let maxNotifyTime = 30

    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {<!-- -->
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
        
        if let bestAttemptContent = bestAttemptContent {<!-- -->
            // Modify the notification content here...
            bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
            
            if let contType = (request.content.userInfo["info"] as? Dictionary<String , Any>)?["type"] as? Int32{<!-- -->
                print("Current message type: \(contType)")
                if contType == 1 {<!-- -->
                    //Audio and video call request
                    if isAppInForground() || isCallNotifyActive() {<!-- -->
                        //When the APP is in the foreground or the call has been activated, internal processing is automatically processed
                        contentHandler(request.content)
                        return
                    }
                    //call invitation
                    bestAttemptContent.title = "\(bestAttemptContent.title)"
                    bestAttemptContent.interruptionLevel = .timeSensitive
                    //create sound
                    var soundID: SystemSoundID = SystemSoundID(clamping: 999)
                    // Play sound
                    if let url = Bundle.main.url(forResource: "ring", withExtension: "mp3"){<!-- -->
                        AudioServicesCreateSystemSoundID(url as CFURL, & amp;soundID)
                        AudioServicesAddSystemSoundCompletion(soundID, nil, nil, {<!-- --> sourceId, unsafeMutableRawPointer in
                        }, nil)
                        AudioServicesPlaySystemSound(soundID)
                        AudioServicesPlaySystemSoundWithCompletion(soundID) {<!-- -->
                            if self.isCallNotifyActive(){<!-- -->
                                print("Not over, continue playing")
                                AudioServicesPlaySystemSound(soundID)
                            }
                        }
                    } else {<!-- -->
                        print("No ringtone found")
                    }
                    
                    contentHandler(bestAttemptContent)
                    //record start time
                    self.setCallStartTime()
 
                    //continuous vibration logic
                    while self.isCallNotifyActive(){<!-- -->
                        AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
                        sleep(1)
                    }
                    self.setCallStartTime(clean: true)
                    AudioServicesDisposeSystemSoundID(soundID)
                }
                else if contType == 2 {<!-- -->
                    //call ended
                    print("Reset task after call ends")
                    setCallStartTime(clean: true)
                    contentHandler(request.content)
                } else {<!-- -->
                    contentHandler(request.content)
                }
            } else {<!-- -->
                contentHandler(request.content)
            }
        }
    }
    
    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.
        if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {<!-- -->
            contentHandler(bestAttemptContent)
        }
    }
    
    //Whether call notification is still active
    func isCallNotifyActive() -> Bool{<!-- -->
        return Int64(Date.now.timeIntervalSince1970) - getStartCallTime() < maxNotifyTime & amp; & amp; !self.isAppInForground()
    }
    
    func getStartCallTime() -> Int64 {<!-- -->
        UserDefaults(suiteName: self.groupName)?.object(forKey: keyStartCallTime) as? Int64  0
    }
    
    func setCallStartTime(clean : Bool = false) {<!-- -->
        if clean {<!-- -->
            UserDefaults(suiteName: self.groupName)?.removeObject(forKey: keyStartCallTime)
        } else {<!-- -->
            let startTime : Int64 = Int64(Date.now.timeIntervalSince1970)
            UserDefaults(suiteName: self.groupName)?.set(startTime, forKey: keyStartCallTime)
        }
    }
    
    //Is the APP in the foreground?
    func isAppInForground() -> Bool{<!-- -->
        UserDefaults(suiteName: self.groupName)?.bool(forKey: callKeyEnterToForeground)  false
    }

}

Troubleshooting issues with notification processing that cannot be intercepted

  • Check the Minimum Deployments version of the interception service. It must be smaller than the version you are running on your mobile phone or emulator. The version specified by default may be the latest version. Just modify the version number

  • When pushing, it depends on the APNS proxy of ios itself. You need to set the mutable-content field in the push content. 1 means on, 0 means off. Only if 1 is set, it will be intercepted by UNNotificationServiceExtension. Please refer to Apple’s official documentation.

Notification service application extension flag. If the value is 1, the notification will be delivered to your notification service app extension before being delivered. Use your extension to modify the notification’s content.

  • Check whether multiple Notification Service Extensions have been created. The test found that only the first Notification Service Extension will take effect, and the remaining Notification Service Extensions will not be used.

Other questions

  • After testing, only the first UNNotificationServiceExtension will take effect, and subsequent UNNotificationServiceExtension will not be called.

  • Ringtone files can be placed in the same directory

  • For multiple notification delivery processing, the test found that when the previous notification has not been processed, the next notification will be queued after arrival, and the didReceive method will not be directly called back until the previous contentHandler (UNNotificationContent) is called. So that it is difficult to start calling the ring and hanging up to stop the ring. However, after testing, it was found that the following methods are available

First execute the contentHandler(UNNotificationContent) callback to prevent UNNotificationServiceExtension from being unable to process the next notification, and then block on the current thread. This can effectively prevent the main program from being suspended again, causing the ring or vibration to be interrupted, and two notifications are issued. Finally, they don’t seem to be using the same object. The principle is not clear yet. If anyone knows, please let me know.