From fe84275cce7c08d4dc1c125a26297ba0e64f7a6e Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 30 Jan 2019 15:27:53 -0700 Subject: [PATCH] Respect audio preferences/throttling --- Signal/src/AppDelegate.m | 9 +- .../Notifications/AppNotifications.swift | 148 ++++++++++-------- .../LegacyNotificationsAdaptee.swift | 19 ++- .../UserNotificationsAdaptee.swift | 12 +- SignalMessaging/environment/OWSSounds.h | 3 +- 5 files changed, 108 insertions(+), 83 deletions(-) diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index af258f619..9fe83a4ef 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -1508,10 +1508,11 @@ static NSTimeInterval launchStartedAt; { OWSLogInfo(@""); [AppReadiness runNowOrWhenAppDidBecomeReady:^() { - // TODO move this into adaptee ? - // we need to respect the in-app notification sound settings, either here - // or maybe it's simpler to do that when building the notification. e.g. if the app - // is in the forground when the notification was sent, we could just *not* add the sound. + // We need to respect the in-app notification sound preference. This method, which is called + // for modern UNUserNotification users, could be a place to do that, but since we'd still + // need to handle this behavior for legacy UINotification users anyway, we "allow" all + // notification options here, and rely on the shared logic in NotificationPresenter to + // honor notification sound preferences for both modern and legacy users. UNNotificationPresentationOptions options = UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; completionHandler(options); diff --git a/Signal/src/UserInterface/Notifications/AppNotifications.swift b/Signal/src/UserInterface/Notifications/AppNotifications.swift index f9a498b25..237158c39 100644 --- a/Signal/src/UserInterface/Notifications/AppNotifications.swift +++ b/Signal/src/UserInterface/Notifications/AppNotifications.swift @@ -108,6 +108,9 @@ extension AppNotificationAction { // avoid notifying a user on their phone while a conversation is actively happening on desktop. let kNotificationDelayForRemoteRead: TimeInterval = 5 +let kAudioNotificationsThrottleCount = 2 +let kAudioNotificationsThrottleInterval: TimeInterval = 5 + protocol NotificationPresenterAdaptee: class { func registerNotificationSettings() -> Promise @@ -118,7 +121,6 @@ protocol NotificationPresenterAdaptee: class { func cancelNotifications(threadId: String) func clearAllNotifications() - var shouldPlaySoundForNotification: Bool { get } var hasReceivedSyncMessageRecently: Bool { get } } @@ -155,8 +157,12 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { return OWSIdentityManager.shared() } + var preferences: OWSPreferences { + return Environment.shared.preferences + } + var previewType: NotificationType { - return Environment.shared.preferences.notificationPreviewType() + return preferences.notificationPreviewType() } // MARK: - @@ -222,13 +228,11 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { AppNotificationUserInfoKey.localCallId: call.localId.uuidString ] - let sound = OWSSound.defaultiOSIncomingRingtone - DispatchQueue.main.async { self.adaptee.notify(category: .incomingCall, body: notificationBody, userInfo: userInfo, - sound: sound, + sound: .defaultiOSIncomingRingtone, replacingIdentifier: call.localId.uuidString) } } @@ -250,19 +254,13 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { return } - let sound: OWSSound? - if shouldPlaySoundForNotification { - sound = OWSSounds.notificationSound(for: thread) - } else { - sound = nil - } - let userInfo = [ AppNotificationUserInfoKey.threadId: threadId, AppNotificationUserInfoKey.localCallId: call.localId.uuidString ] DispatchQueue.main.async { + let sound = self.requestSound(thread: thread) self.adaptee.notify(category: .missedCall, body: notificationBody, userInfo: userInfo, @@ -287,18 +285,12 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { return } - let sound: OWSSound? - if shouldPlaySoundForNotification { - sound = OWSSounds.notificationSound(for: thread) - } else { - sound = nil - } - let userInfo = [ AppNotificationUserInfoKey.threadId: threadId ] DispatchQueue.main.async { + let sound = self.requestSound(thread: thread) self.adaptee.notify(category: .missedCallFromNoLongerVerifiedIdentity, body: notificationBody, userInfo: userInfo, @@ -325,19 +317,13 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { return } - let sound: OWSSound? - if shouldPlaySoundForNotification { - sound = OWSSounds.notificationSound(for: thread) - } else { - sound = nil - } - let userInfo = [ AppNotificationUserInfoKey.threadId: threadId, AppNotificationUserInfoKey.callBackNumber: remotePhoneNumber ] DispatchQueue.main.async { + let sound = self.requestSound(thread: thread) self.adaptee.notify(category: .missedCall, body: notificationBody, userInfo: userInfo, @@ -407,13 +393,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { } } - let sound: OWSSound? - if shouldPlaySoundForNotification { - sound = OWSSounds.notificationSound(for: thread) - } else { - sound = nil - } - guard let threadId = thread.uniqueId else { owsFailDebug("threadId was unexpectedly nil") return @@ -434,6 +413,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { ] DispatchQueue.main.async { + let sound = self.requestSound(thread: thread) self.adaptee.notify(category: category, body: notificationBody, userInfo: userInfo, sound: sound) } } @@ -442,13 +422,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { let notificationFormat = NSLocalizedString("NOTIFICATION_SEND_FAILED", comment: "subsequent notification body when replying from notification fails") let notificationBody = String(format: notificationFormat, thread.name()) - let sound: OWSSound? - if shouldPlaySoundForNotification { - sound = OWSSounds.notificationSound(for: thread) - } else { - sound = nil - } - guard let threadId = thread.uniqueId else { owsFailDebug("threadId was unexpectedly nil") return @@ -459,6 +432,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { ] DispatchQueue.main.async { + let sound = self.requestSound(thread: thread) self.adaptee.notify(category: .errorMessage, body: notificationBody, userInfo: userInfo, sound: sound) } } @@ -476,13 +450,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { notificationBody = messageText } - let sound: OWSSound? - if shouldPlaySoundForNotification { - sound = OWSSounds.notificationSound(for: thread) - } else { - sound = nil - } - guard let threadId = thread.uniqueId else { owsFailDebug("threadId was unexpectedly nil") return @@ -493,6 +460,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { ] transaction.addCompletionQueue(DispatchQueue.main) { + let sound = self.requestSound(thread: thread) self.adaptee.notify(category: .errorMessage, body: notificationBody, userInfo: userInfo, sound: sound) } } @@ -500,14 +468,8 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { public func notifyUser(forThreadlessErrorMessage errorMessage: TSErrorMessage, transaction: YapDatabaseReadWriteTransaction) { let notificationBody = errorMessage.previewText(with: transaction) - let sound: OWSSound? - if shouldPlaySoundForNotification { - sound = OWSSounds.globalNotificationSound() - } else { - sound = nil - } - transaction.addCompletionQueue(DispatchQueue.main) { + let sound = self.checkIfShouldPlaySound() ? OWSSounds.globalNotificationSound() : nil self.adaptee.notify(category: .threadlessErrorMessage, body: notificationBody, userInfo: [:], sound: sound) } } @@ -520,9 +482,40 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { adaptee.clearAllNotifications() } - // TODO rename to something like 'shouldThrottle' or 'requestAudioUsage' - var shouldPlaySoundForNotification: Bool { - return adaptee.shouldPlaySoundForNotification + // MARK: - + + var mostRecentNotifications = TruncatedList(maxLength: kAudioNotificationsThrottleCount) + + private func requestSound(thread: TSThread) -> OWSSound? { + guard checkIfShouldPlaySound() else { + return nil + } + + return OWSSounds.notificationSound(for: thread) + } + + private func checkIfShouldPlaySound() -> Bool { + AssertIsOnMainThread() + + guard UIApplication.shared.applicationState == .active else { + return true + } + + guard preferences.soundInForeground() else { + return false + } + + let now = NSDate.ows_millisecondTimeStamp() + let recentThreshold = now - UInt64(kAudioNotificationsThrottleInterval * Double(kSecondInMs)) + + let recentNotifications = mostRecentNotifications.filter { $0 > recentThreshold } + + guard recentNotifications.count < kAudioNotificationsThrottleCount else { + return false + } + + mostRecentNotifications.append(now) + return true } } @@ -654,12 +647,6 @@ extension ThreadUtil { } } -extension OWSSound { - var filename: String? { - return OWSSounds.filename(for: self) - } -} - enum NotificationError: Error { case assertionError(description: String) } @@ -670,3 +657,38 @@ extension NotificationError { return NotificationError.assertionError(description: description) } } + +struct TruncatedList { + let maxLength: Int + private var contents: [Element] = [] + + init(maxLength: Int) { + self.maxLength = maxLength + } + + mutating func append(_ newElement: Element) { + var newElements = self.contents + newElements.append(newElement) + self.contents = Array(newElements.suffix(maxLength)) + } +} + +extension TruncatedList: Collection { + typealias Index = Int + + var startIndex: Index { + return contents.startIndex + } + + var endIndex: Index { + return contents.endIndex + } + + subscript (position: Index) -> Element { + return contents[position] + } + + func index(after i: Index) -> Index { + return contents.index(after: i) + } +} diff --git a/Signal/src/UserInterface/Notifications/LegacyNotificationsAdaptee.swift b/Signal/src/UserInterface/Notifications/LegacyNotificationsAdaptee.swift index 295d2493d..205086e79 100644 --- a/Signal/src/UserInterface/Notifications/LegacyNotificationsAdaptee.swift +++ b/Signal/src/UserInterface/Notifications/LegacyNotificationsAdaptee.swift @@ -145,7 +145,12 @@ extension LegacyNotificationPresenterAdaptee: NotificationPresenterAdaptee { func notify(category: AppNotificationCategory, body: String, userInfo: [AnyHashable: Any], sound: OWSSound?, replacingIdentifier: String?) { AssertIsOnMainThread() guard UIApplication.shared.applicationState != .active else { - Logger.info("skipping notification; app is in foreground") + if let sound = sound { + let soundId = OWSSounds.systemSoundID(for: sound, quiet: true) + + // Vibrate, respect silent switch, respect "Alert" volume, not media volume. + AudioServicesPlayAlertSound(soundId) + } return } @@ -210,12 +215,6 @@ extension LegacyNotificationPresenterAdaptee: NotificationPresenterAdaptee { cancelNotification(notification) } } - - // FIXME: Accomodate 'playSoundsInForeground' preference - // FIXME: debounce - var shouldPlaySoundForNotification: Bool { - return true - } } @objc @@ -288,3 +287,9 @@ public class LegacyNotificationActionHandler: NSObject { } } } + +extension OWSSound { + var filename: String? { + return OWSSounds.filename(for: self, quiet: false) + } +} diff --git a/Signal/src/UserInterface/Notifications/UserNotificationsAdaptee.swift b/Signal/src/UserInterface/Notifications/UserNotificationsAdaptee.swift index b9a8a7ffa..74ded3168 100644 --- a/Signal/src/UserInterface/Notifications/UserNotificationsAdaptee.swift +++ b/Signal/src/UserInterface/Notifications/UserNotificationsAdaptee.swift @@ -112,7 +112,8 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee { let content = UNMutableNotificationContent() content.categoryIdentifier = category.identifier content.userInfo = userInfo - content.sound = sound?.notificationSound + let isAppActive = UIApplication.shared.applicationState == .active + content.sound = sound?.notificationSound(isQuiet: isAppActive) var notificationIdentifier: String = UUID().uuidString if let replacingIdentifier = replacingIdentifier { @@ -177,11 +178,6 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee { notificationCenter.removeAllDeliveredNotifications() } - // UNUserNotification framework does it's own audio throttling - var shouldPlaySoundForNotification: Bool { - return true - } - func shouldPresentNotification(category: AppNotificationCategory, userInfo: [AnyHashable: Any]) -> Bool { AssertIsOnMainThread() guard UIApplication.shared.applicationState == .active else { @@ -278,8 +274,8 @@ public class UserNotificationActionHandler: NSObject { extension OWSSound { @available(iOS 10.0, *) - var notificationSound: UNNotificationSound { - guard let filename = OWSSounds.filename(for: self) else { + func notificationSound(isQuiet: Bool) -> UNNotificationSound { + guard let filename = OWSSounds.filename(for: self, quiet: isQuiet) else { owsFailDebug("filename was unexpectedly nil") return UNNotificationSound.default() } diff --git a/SignalMessaging/environment/OWSSounds.h b/SignalMessaging/environment/OWSSounds.h index f7ff36072..373f0e5cf 100644 --- a/SignalMessaging/environment/OWSSounds.h +++ b/SignalMessaging/environment/OWSSounds.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "OWSAudioPlayer.h" @@ -54,6 +54,7 @@ typedef NS_ENUM(NSUInteger, OWSSound) { + (NSString *)displayNameForSound:(OWSSound)sound; + (nullable NSString *)filenameForSound:(OWSSound)sound; ++ (nullable NSString *)filenameForSound:(OWSSound)sound quiet:(BOOL)quiet; #pragma mark - Notifications