|
|
|
//
|
|
|
|
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
#import "OWSSounds.h"
|
|
|
|
#import "OWSAudioPlayer.h"
|
|
|
|
#import <SignalServiceKit/OWSFileSystem.h>
|
|
|
|
#import <SignalServiceKit/TSStorageManager.h>
|
|
|
|
#import <SignalServiceKit/TSThread.h>
|
|
|
|
#import <SignalServiceKit/YapDatabaseConnection+OWS.h>
|
|
|
|
#import <YapDatabase/YapDatabase.h>
|
|
|
|
|
|
|
|
NSString *const kOWSSoundsStorageNotificationCollection = @"kOWSSoundsStorageNotificationCollection";
|
|
|
|
NSString *const kOWSSoundsStorageGlobalNotificationKey = @"kOWSSoundsStorageGlobalNotificationKey";
|
|
|
|
|
|
|
|
@interface OWSSounds ()
|
|
|
|
|
|
|
|
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
|
|
|
@implementation OWSSounds
|
|
|
|
|
|
|
|
+ (instancetype)sharedManager
|
|
|
|
{
|
|
|
|
static OWSSounds *instance = nil;
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
instance = [[self alloc] initDefault];
|
|
|
|
});
|
|
|
|
return instance;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (instancetype)initDefault
|
|
|
|
{
|
|
|
|
TSStorageManager *storageManager = [TSStorageManager sharedManager];
|
|
|
|
|
|
|
|
return [self initWithStorageManager:storageManager];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (instancetype)initWithStorageManager:(TSStorageManager *)storageManager
|
|
|
|
{
|
|
|
|
self = [super init];
|
|
|
|
|
|
|
|
if (!self) {
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
OWSAssert(storageManager);
|
|
|
|
|
|
|
|
_dbConnection = storageManager.newDatabaseConnection;
|
|
|
|
|
|
|
|
OWSSingletonAssert();
|
|
|
|
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (NSArray<NSNumber *> *)allNotificationSounds
|
|
|
|
{
|
|
|
|
return @[
|
|
|
|
// None and Note (default) should be first.
|
|
|
|
@(OWSSound_None),
|
|
|
|
@(OWSSound_Note),
|
|
|
|
|
|
|
|
@(OWSSound_Aurora),
|
|
|
|
@(OWSSound_Bamboo),
|
|
|
|
@(OWSSound_Chord),
|
|
|
|
@(OWSSound_Circles),
|
|
|
|
@(OWSSound_Complete),
|
|
|
|
@(OWSSound_Hello),
|
|
|
|
@(OWSSound_Input),
|
|
|
|
@(OWSSound_Keys),
|
|
|
|
@(OWSSound_Popcorn),
|
|
|
|
@(OWSSound_Pulse),
|
|
|
|
@(OWSSound_Synth),
|
|
|
|
@(OWSSound_ClassicNotification),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (NSString *)displayNameForSound:(OWSSound)sound
|
|
|
|
{
|
|
|
|
// TODO: Should we localize these sound names?
|
|
|
|
switch (sound) {
|
|
|
|
case OWSSound_Default:
|
|
|
|
OWSFail(@"%@ invalid argument.", self.logTag);
|
|
|
|
return @"";
|
|
|
|
|
|
|
|
// Notification Sounds
|
|
|
|
case OWSSound_Aurora:
|
|
|
|
return @"Aurora";
|
|
|
|
case OWSSound_Bamboo:
|
|
|
|
return @"Bamboo";
|
|
|
|
case OWSSound_Chord:
|
|
|
|
return @"Chord";
|
|
|
|
case OWSSound_Circles:
|
|
|
|
return @"Circles";
|
|
|
|
case OWSSound_Complete:
|
|
|
|
return @"Complete";
|
|
|
|
case OWSSound_Hello:
|
|
|
|
return @"Hello";
|
|
|
|
case OWSSound_Input:
|
|
|
|
return @"Input";
|
|
|
|
case OWSSound_Keys:
|
|
|
|
return @"Keys";
|
|
|
|
case OWSSound_Note:
|
|
|
|
return @"Note";
|
|
|
|
case OWSSound_Popcorn:
|
|
|
|
return @"Popcorn";
|
|
|
|
case OWSSound_Pulse:
|
|
|
|
return @"Pulse";
|
|
|
|
case OWSSound_Synth:
|
|
|
|
return @"Synth";
|
|
|
|
case OWSSound_ClassicNotification:
|
|
|
|
return @"Signal Classic";
|
|
|
|
|
|
|
|
// Call Audio
|
|
|
|
case OWSSound_Opening:
|
|
|
|
return @"Opening";
|
|
|
|
case OWSSound_CallConnecting:
|
|
|
|
return @"Call Connecting";
|
|
|
|
case OWSSound_CallOutboundRinging:
|
|
|
|
return @"Call Outboung Ringing";
|
|
|
|
case OWSSound_CallBusy:
|
|
|
|
return @"Call Busy";
|
|
|
|
case OWSSound_CallFailure:
|
|
|
|
return @"Call Failure";
|
|
|
|
|
|
|
|
// Other
|
|
|
|
case OWSSound_None:
|
|
|
|
return NSLocalizedString(@"SOUNDS_NONE",
|
|
|
|
@"Label for the 'no sound' option that allows users to disable sounds for notifications, "
|
|
|
|
@"etc.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (nullable NSString *)filenameForSound:(OWSSound)sound
|
|
|
|
{
|
|
|
|
return [self filenameForSound:sound quiet:NO];
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (nullable NSString *)filenameForSound:(OWSSound)sound quiet:(BOOL)quiet
|
|
|
|
{
|
|
|
|
switch (sound) {
|
|
|
|
case OWSSound_Default:
|
|
|
|
OWSFail(@"%@ invalid argument.", self.logTag);
|
|
|
|
return @"";
|
|
|
|
|
|
|
|
// Notification Sounds
|
|
|
|
case OWSSound_Aurora:
|
|
|
|
return (quiet ? @"aurora-quiet.aifc" : @"aurora.aifc");
|
|
|
|
case OWSSound_Bamboo:
|
|
|
|
return (quiet ? @"bamboo-quiet.aifc" : @"bamboo.aifc");
|
|
|
|
case OWSSound_Chord:
|
|
|
|
return (quiet ? @"chord-quiet.aifc" : @"chord.aifc");
|
|
|
|
case OWSSound_Circles:
|
|
|
|
return (quiet ? @"circles-quiet.aifc" : @"circles.aifc");
|
|
|
|
case OWSSound_Complete:
|
|
|
|
return (quiet ? @"complete-quiet.aifc" : @"complete.aifc");
|
|
|
|
case OWSSound_Hello:
|
|
|
|
return (quiet ? @"hello-quiet.aifc" : @"hello.aifc");
|
|
|
|
case OWSSound_Input:
|
|
|
|
return (quiet ? @"input-quiet.aifc" : @"input.aifc");
|
|
|
|
case OWSSound_Keys:
|
|
|
|
return (quiet ? @"keys-quiet.aifc" : @"keys.aifc");
|
|
|
|
case OWSSound_Note:
|
|
|
|
return (quiet ? @"note-quiet.aifc" : @"note.aifc");
|
|
|
|
case OWSSound_Popcorn:
|
|
|
|
return (quiet ? @"popcorn-quiet.aifc" : @"popcorn.aifc");
|
|
|
|
case OWSSound_Pulse:
|
|
|
|
return (quiet ? @"pulse-quiet.aifc" : @"pulse.aifc");
|
|
|
|
case OWSSound_Synth:
|
|
|
|
return (quiet ? @"synth-quiet.aifc" : @"synth.aifc");
|
|
|
|
case OWSSound_ClassicNotification:
|
|
|
|
return (quiet ? @"classic-quiet.aifc" : @"classic.aifc");
|
|
|
|
|
|
|
|
// Ringtone Sounds
|
|
|
|
case OWSSound_Opening:
|
|
|
|
return @"Opening.m4r";
|
|
|
|
|
|
|
|
// Calls
|
|
|
|
case OWSSound_CallConnecting:
|
|
|
|
return @"sonarping.mp3";
|
|
|
|
case OWSSound_CallOutboundRinging:
|
|
|
|
return @"ringback_tone_ansi.caf";
|
|
|
|
case OWSSound_CallBusy:
|
|
|
|
return @"busy_tone_ansi.caf";
|
|
|
|
case OWSSound_CallFailure:
|
|
|
|
return @"end_call_tone_cept.caf";
|
|
|
|
|
|
|
|
// Other
|
|
|
|
case OWSSound_None:
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (nullable NSURL *)soundURLForSound:(OWSSound)sound quiet:(BOOL)quiet
|
|
|
|
{
|
|
|
|
NSString *_Nullable filename = [self filenameForSound:sound quiet:quiet];
|
|
|
|
if (!filename) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
NSURL *_Nullable url = [[NSBundle mainBundle] URLForResource:filename.stringByDeletingPathExtension
|
|
|
|
withExtension:filename.pathExtension];
|
|
|
|
OWSAssert(url);
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - Notifications
|
|
|
|
|
|
|
|
+ (OWSSound)defaultNotificationSound
|
|
|
|
{
|
|
|
|
return OWSSound_Note;
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (OWSSound)globalNotificationSound
|
|
|
|
{
|
|
|
|
OWSSounds *instance = OWSSounds.sharedManager;
|
|
|
|
NSNumber *_Nullable value = [instance.dbConnection objectForKey:kOWSSoundsStorageGlobalNotificationKey
|
|
|
|
inCollection:kOWSSoundsStorageNotificationCollection];
|
|
|
|
// Default to the global default.
|
|
|
|
return (value ? (OWSSound)value.intValue : [self defaultNotificationSound]);
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (void)setGlobalNotificationSound:(OWSSound)sound
|
|
|
|
{
|
|
|
|
[self.sharedManager setGlobalNotificationSound:sound];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)setGlobalNotificationSound:(OWSSound)sound
|
|
|
|
{
|
|
|
|
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
|
|
|
[self setGlobalNotificationSound:sound transaction:transaction];
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (void)setGlobalNotificationSound:(OWSSound)sound transaction:(YapDatabaseReadWriteTransaction *)transaction
|
|
|
|
{
|
|
|
|
[self.sharedManager setGlobalNotificationSound:sound transaction:transaction];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)setGlobalNotificationSound:(OWSSound)sound transaction:(YapDatabaseReadWriteTransaction *)transaction
|
|
|
|
{
|
|
|
|
OWSAssert(transaction);
|
|
|
|
|
|
|
|
DDLogInfo(@"%@ Setting global notification sound to: %@", self.logTag, [[self class] displayNameForSound:sound]);
|
|
|
|
|
|
|
|
// Fallback push notifications play a sound specified by the server, but we don't want to store this configuration
|
|
|
|
// on the server. Instead, we create a file with the same name as the default to be played when receiving
|
|
|
|
// a fallback notification.
|
|
|
|
NSString *dirPath = [[OWSFileSystem appLibraryDirectoryPath] stringByAppendingPathComponent:@"Sounds"];
|
|
|
|
[OWSFileSystem ensureDirectoryExists:dirPath];
|
|
|
|
|
|
|
|
// This name is specified in the payload by the Signal Service when requesting fallback push notifications.
|
|
|
|
NSString *kDefaultNotificationSoundFilename = @"NewMessage.aifc";
|
|
|
|
NSString *defaultSoundPath = [dirPath stringByAppendingPathComponent:kDefaultNotificationSoundFilename];
|
|
|
|
|
|
|
|
DDLogDebug(@"%@ writing new default sound to %@", self.logTag, defaultSoundPath);
|
|
|
|
|
|
|
|
NSURL *_Nullable soundURL = [OWSSounds soundURLForSound:sound quiet:NO];
|
|
|
|
|
|
|
|
NSData *soundData = ^{
|
|
|
|
if (soundURL) {
|
|
|
|
return [NSData dataWithContentsOfURL:soundURL];
|
|
|
|
} else {
|
|
|
|
OWSAssert(sound == OWSSound_None);
|
|
|
|
return [NSData new];
|
|
|
|
}
|
|
|
|
}();
|
|
|
|
|
|
|
|
// Quick way to achieve an atomic "copy" operation that allows overwriting if the user has previously specified
|
|
|
|
// a default notification sound.
|
|
|
|
BOOL success = [soundData writeToFile:defaultSoundPath atomically:YES];
|
|
|
|
|
|
|
|
// The globally configured sound the user has configured is unprotected, so that we can still play the sound if the
|
|
|
|
// user hasn't authenticated after power-cycling their device.
|
|
|
|
[OWSFileSystem protectFileOrFolderAtPath:defaultSoundPath fileProtectionType:NSFileProtectionNone];
|
|
|
|
|
|
|
|
if (!success) {
|
|
|
|
OWSProdLogAndFail(
|
|
|
|
@"%@ Unable to write new default sound data from: %@ to :%@", self.logTag, soundURL, defaultSoundPath);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
[transaction setObject:@(sound)
|
|
|
|
forKey:kOWSSoundsStorageGlobalNotificationKey
|
|
|
|
inCollection:kOWSSoundsStorageNotificationCollection];
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (OWSSound)notificationSoundForThread:(TSThread *)thread
|
|
|
|
{
|
|
|
|
OWSSounds *instance = OWSSounds.sharedManager;
|
|
|
|
NSNumber *_Nullable value =
|
|
|
|
[instance.dbConnection objectForKey:thread.uniqueId inCollection:kOWSSoundsStorageNotificationCollection];
|
|
|
|
// Default to the "global" notification sound, which in turn will default to the global default.
|
|
|
|
return (value ? (OWSSound)value.intValue : [self globalNotificationSound]);
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (void)setNotificationSound:(OWSSound)sound forThread:(TSThread *)thread
|
|
|
|
{
|
|
|
|
OWSSounds *instance = OWSSounds.sharedManager;
|
|
|
|
[instance.dbConnection setObject:@(sound)
|
|
|
|
forKey:thread.uniqueId
|
|
|
|
inCollection:kOWSSoundsStorageNotificationCollection];
|
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - AudioPlayer
|
|
|
|
|
|
|
|
+ (BOOL)shouldAudioPlayerLoopForSound:(OWSSound)sound
|
|
|
|
{
|
|
|
|
return (sound == OWSSound_CallConnecting || sound == OWSSound_CallOutboundRinging);
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (nullable OWSAudioPlayer *)audioPlayerForSound:(OWSSound)sound
|
|
|
|
{
|
|
|
|
return [self audioPlayerForSound:sound quiet:NO];
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (nullable OWSAudioPlayer *)audioPlayerForSound:(OWSSound)sound quiet:(BOOL)quiet
|
|
|
|
{
|
|
|
|
NSURL *_Nullable soundURL = [OWSSounds soundURLForSound:sound quiet:(BOOL)quiet];
|
|
|
|
if (!soundURL) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
OWSAudioPlayer *player = [[OWSAudioPlayer alloc] initWithMediaUrl:soundURL];
|
|
|
|
if ([self shouldAudioPlayerLoopForSound:sound]) {
|
|
|
|
player.isLooping = YES;
|
|
|
|
}
|
|
|
|
return player;
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|