兼容 call=1 和 level=critical

close: #256
This commit is contained in:
Fin 2025-01-17 11:13:51 +08:00
parent 9ca2577ad8
commit f55a776874
4 changed files with 132 additions and 142 deletions

View File

@ -100,9 +100,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
if UIApplication.shared.applicationState == .active {
stopCallNotificationProcessor()
}
return .alert
}
@ -199,11 +196,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
UIApplication.shared.applicationIconBadgeNumber = -1
}
func applicationDidBecomeActive(_ application: UIApplication) {
//
stopCallNotificationProcessor()
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
@ -226,9 +218,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
return false
}
///
func stopCallNotificationProcessor() {
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), CFNotificationName(kStopCallProcessorKey as CFString), nil, nil, true)
}
}

View File

@ -10,3 +10,4 @@ import Foundation
let kStopCallProcessorKey = "stopCallProcessorNotification"
let kBarkSoundPrefix = "bark.sounds.30s"

View File

@ -7,82 +7,31 @@
//
import AudioToolbox
import AVFAudio
import Foundation
class CallProcessor: NotificationContentProcessor {
///
var soundID: SystemSoundID = 0
/// content
var content: UNMutableNotificationContent? = nil
/// APP
var needsStop = false
/// 访APP
let soundsDirectoryUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark")?.appendingPathComponent("Library/Sounds")
func process(identifier: String, content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {
guard let call = bestAttemptContent.userInfo["call"] as? String, call == "1" else {
return bestAttemptContent
}
self.content = bestAttemptContent
// 30s
return self.processNotificationSound(content: bestAttemptContent)
}
}
self.registerObserver()
self.sendLocalNotification(identifier: identifier, content: bestAttemptContent)
self.cancelRemoteNotification(content: bestAttemptContent)
await startAudioWork()
return bestAttemptContent
}
func serviceExtensionTimeWillExpire(contentHandler: (UNNotificationContent) -> Void) {
stopAudioWork()
if let content {
contentHandler(content)
}
}
///
private func sendLocalNotification(identifier: String, content: UNMutableNotificationContent) {
// id使APNS
guard let content = content.mutableCopy() as? UNMutableNotificationContent else {
return
}
if !content.isCritical { //
content.sound = nil
}
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
}
///
private func cancelRemoteNotification(content: UNMutableNotificationContent) {
//
// iOS15
// level level
if #available(iOSApplicationExtension 15.0, *), self.content?.userInfo["level"] == nil {
self.content?.interruptionLevel = .passive
}
}
// startAudioWork(completion:)
private func startAudioWork() async {
return await withCheckedContinuation { continuation in
self.startAudioWork {
continuation.resume()
}
}
}
///
var startAudioWorkCompletion: (() -> Void)? = nil
///
private func startAudioWork(completion: @escaping () -> Void) {
guard let content else {
completion()
return
}
self.startAudioWorkCompletion = completion
// MARK: -
extension CallProcessor {
// 30s30s
func processNotificationSound(content: UNMutableNotificationContent) -> UNMutableNotificationContent {
let sound = ((content.userInfo["aps"] as? [String: Any])?["sound"] as? String)?.split(separator: ".")
let soundName: String
let soundType: String
if sound?.count == 2, let first = sound?.first, let last = sound?.last {
if sound?.count == 2, let first = sound?.first, let last = sound?.last, last == "caf" {
soundName = String(first)
soundType = String(last)
} else {
@ -90,65 +39,108 @@ class CallProcessor: NotificationContentProcessor {
soundType = "caf"
}
//
guard let audioPath = getSoundInCustomSoundsDirectory(soundName: "\(soundName).\(soundType)") ??
Bundle.main.path(forResource: soundName, ofType: soundType)
else {
completion()
return
if let longSoundUrl = getLongSound(soundName: soundName, soundType: soundType) {
if let level = content.userInfo["level"] as? String, level == "critical" {
LevelProcessor.setCriticalSound(content: content, soundName: longSoundUrl.lastPathComponent)
} else {
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: longSoundUrl.lastPathComponent))
}
}
return content
}
let fileUrl = URL(string: audioPath)
//
AudioServicesCreateSystemSoundID(fileUrl! as CFURL, &soundID)
//
AudioServicesPlayAlertSound(soundID)
//
let selfPointer = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
AudioServicesAddSystemSoundCompletion(soundID, nil, nil, { sound, clientData in
guard let pointer = clientData else { return }
let processor = unsafeBitCast(pointer, to: CallProcessor.self)
if processor.needsStop {
processor.startAudioWorkCompletion?()
return
}
//
AudioServicesPlayAlertSound(sound)
}, selfPointer)
}
///
private func stopAudioWork() {
AudioServicesRemoveSystemSoundCompletion(soundID)
AudioServicesDisposeSystemSoundID(soundID)
}
///
func registerObserver() {
let notification = CFNotificationCenterGetDarwinNotifyCenter()
let observer = Unmanaged.passUnretained(self).toOpaque()
CFNotificationCenterAddObserver(notification, observer, { _, pointer, _, _, _ in
guard let observer = pointer else { return }
let processor = Unmanaged<CallProcessor>.fromOpaque(observer).takeUnretainedValue()
processor.needsStop = true
}, kStopCallProcessorKey as CFString, nil, .deliverImmediately)
}
func getSoundInCustomSoundsDirectory(soundName: String) -> String? {
// 访APP
guard let soundsDirectoryUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark")?.appendingPathComponent("Sounds") else {
func getLongSound(soundName: String, soundType: String) -> URL? {
guard let soundsDirectoryUrl else {
return nil
}
let path = soundsDirectoryUrl.appendingPathComponent(soundName).path
if FileManager.default.fileExists(atPath: path) {
return path
//
if !FileManager.default.fileExists(atPath: soundsDirectoryUrl.path) {
try? FileManager.default.createDirectory(atPath: soundsDirectoryUrl.path, withIntermediateDirectories: true, attributes: nil)
}
//
let longSoundName = "\(kBarkSoundPrefix).\(soundName).\(soundType)"
let longSoundPath = soundsDirectoryUrl.appendingPathComponent(longSoundName)
if FileManager.default.fileExists(atPath: longSoundPath.path) {
return longSoundPath
}
//
var path: String = soundsDirectoryUrl.appendingPathComponent("\(soundName).\(soundType)").path
if !FileManager.default.fileExists(atPath: path) {
//
path = Bundle.main.path(forResource: soundName, ofType: soundType) ?? ""
}
guard !path.isEmpty else {
return nil
}
deinit {
let observer = Unmanaged.passUnretained(self).toOpaque()
let name = CFNotificationName(kStopCallProcessorKey as CFString)
CFNotificationCenterRemoveObserver(CFNotificationCenterGetDarwinNotifyCenter(), observer, name, nil)
// 30s
return mergeCAFFilesToDuration(inputFile: URL(fileURLWithPath: path))
}
/// - Author: @uuneo
/// - Description:
/// - Parameters:
/// - inputFile:
/// - targetDuration:
/// - Returns:
func mergeCAFFilesToDuration(inputFile: URL, targetDuration: TimeInterval = 30) -> URL? {
guard let soundsDirectoryUrl else {
return nil
}
let longSoundName = "\(kBarkSoundPrefix).\(inputFile.lastPathComponent)"
let longSoundPath = soundsDirectoryUrl.appendingPathComponent(longSoundName)
do {
//
let audioFile = try AVAudioFile(forReading: inputFile)
let audioFormat = audioFile.processingFormat
let sampleRate = audioFormat.sampleRate
//
let targetFrames = AVAudioFramePosition(targetDuration * sampleRate)
var currentFrames: AVAudioFramePosition = 0
//
let outputAudioFile = try AVAudioFile(forWriting: longSoundPath, settings: audioFormat.settings)
//
while currentFrames < targetFrames {
//
guard let buffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: AVAudioFrameCount(audioFile.length)) else {
//
return nil
}
try audioFile.read(into: buffer)
//
let remainingFrames = targetFrames - currentFrames
if AVAudioFramePosition(buffer.frameLength) > remainingFrames {
//
let truncatedBuffer = AVAudioPCMBuffer(pcmFormat: buffer.format, frameCapacity: AVAudioFrameCount(remainingFrames))!
let channelCount = Int(buffer.format.channelCount)
for channel in 0..<channelCount {
let sourcePointer = buffer.floatChannelData![channel]
let destinationPointer = truncatedBuffer.floatChannelData![channel]
memcpy(destinationPointer, sourcePointer, Int(remainingFrames) * MemoryLayout<Float>.size)
}
truncatedBuffer.frameLength = AVAudioFrameCount(remainingFrames)
try outputAudioFile.write(from: truncatedBuffer)
break
} else {
//
try outputAudioFile.write(from: buffer)
currentFrames += AVAudioFramePosition(buffer.frameLength)
}
//
audioFile.framePosition = 0
}
return longSoundPath
} catch {
print("Error processing CAF file: \(error)")
return nil
}
}
}

View File

@ -15,20 +15,9 @@ class LevelProcessor: NotificationContentProcessor {
return bestAttemptContent
}
//
if level == "critical" {
//
var audioVolume: Float = 0.5
// 0 - 10 , 0.0 - 1.0
if let volume = bestAttemptContent.userInfo["volume"] as? String, let volume = Float(volume) {
audioVolume = max(0.0, min(1, volume / 10.0))
}
// sound
if let sound = bestAttemptContent.soundName {
bestAttemptContent.sound = UNNotificationSound.criticalSoundNamed(UNNotificationSoundName(rawValue: sound), withAudioVolume: audioVolume)
} else {
bestAttemptContent.sound = UNNotificationSound.defaultCriticalSound(withAudioVolume: audioVolume)
}
if let level = bestAttemptContent.userInfo["level"] as? String, level == "critical" {
//
LevelProcessor.setCriticalSound(content: bestAttemptContent)
return bestAttemptContent
}
@ -48,6 +37,27 @@ class LevelProcessor: NotificationContentProcessor {
}
}
extension LevelProcessor {
class func setCriticalSound(content bestAttemptContent: UNMutableNotificationContent, soundName: String? = nil) {
guard let level = bestAttemptContent.userInfo["level"] as? String, level == "critical" else {
return
}
//
var audioVolume: Float = 0.5
// 0 - 10 , 0.0 - 1.0
if let volume = bestAttemptContent.userInfo["volume"] as? String, let volume = Float(volume) {
audioVolume = max(0.0, min(1, volume / 10.0))
}
// sound
let sound = soundName ?? bestAttemptContent.soundName
if let sound {
bestAttemptContent.sound = UNNotificationSound.criticalSoundNamed(UNNotificationSoundName(rawValue: sound), withAudioVolume: audioVolume)
} else {
bestAttemptContent.sound = UNNotificationSound.defaultCriticalSound(withAudioVolume: audioVolume)
}
}
}
extension UNMutableNotificationContent {
///
var isCritical: Bool {