session-ios/SessionMessagingKit/Utilities/OWSSounds.m

366 lines
11 KiB
Objective-C

//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSSounds.h"
#import "Environment.h"
#import "OWSAudioPlayer.h"
#import <SessionUtilitiesKit/SessionUtilitiesKit.h>
#import <SessionMessagingKit/OWSPrimaryStorage.h>
#import <SessionMessagingKit/TSThread.h>
#import <SessionMessagingKit/YapDatabaseConnection+OWS.h>
#import <YapDatabase/YapDatabase.h>
NSString *const kOWSSoundsStorageNotificationCollection = @"kOWSSoundsStorageNotificationCollection";
NSString *const kOWSSoundsStorageGlobalNotificationKey = @"kOWSSoundsStorageGlobalNotificationKey";
@interface OWSSystemSound : NSObject
@property (nonatomic, readonly) SystemSoundID soundID;
@property (nonatomic, readonly) NSURL *soundURL;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithURL:(NSURL *)url NS_DESIGNATED_INITIALIZER;
@end
@implementation OWSSystemSound
- (instancetype)initWithURL:(NSURL *)url
{
self = [super init];
if (!self) {
return self;
}
_soundURL = url;
SystemSoundID newSoundID;
_soundID = newSoundID;
return self;
}
- (void)dealloc
{
}
@end
@interface OWSSounds ()
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@property (nonatomic, readonly) AnyLRUCache *cachedSystemSounds;
@end
#pragma mark -
@implementation OWSSounds
+ (instancetype)sharedManager
{
return Environment.shared.sounds;
}
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage
{
self = [super init];
if (!self) {
return self;
}
_dbConnection = primaryStorage.newDatabaseConnection;
// Don't store too many sounds in memory. Most users will only use 1 or 2 sounds anyway.
_cachedSystemSounds = [[AnyLRUCache alloc] initWithMaxSize:4];
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),
];
}
+ (NSString *)displayNameForSound:(OWSSound)sound
{
// TODO: Should we localize these sound names?
switch (sound) {
case OWSSound_Default:
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_SignalClassic:
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";
case OWSSound_MessageSent:
return @"Message Sent";
// 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:
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_SignalClassic:
return (quiet ? @"classic-quiet.aifc" : @"classic.aifc");
// Ringtone Sounds
case OWSSound_Opening:
return @"Opening.m4r";
// Calls
case OWSSound_CallConnecting:
return @"ringback_tone_ansi.caf";
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";
case OWSSound_MessageSent:
return @"message_sent.aiff";
// 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];
return url;
}
+ (SystemSoundID)systemSoundIDForSound:(OWSSound)sound quiet:(BOOL)quiet
{
return [self.sharedManager systemSoundIDForSound:(OWSSound)sound quiet:quiet];
}
- (SystemSoundID)systemSoundIDForSound:(OWSSound)sound quiet:(BOOL)quiet
{
NSString *cacheKey = [NSString stringWithFormat:@"%lu:%d", (unsigned long)sound, quiet];
OWSSystemSound *_Nullable cachedSound = (OWSSystemSound *)[self.cachedSystemSounds getWithKey:cacheKey];
if (cachedSound) {
return cachedSound.soundID;
}
NSURL *soundURL = [self.class soundURLForSound:sound quiet:quiet];
OWSSystemSound *newSound = [[OWSSystemSound alloc] initWithURL:soundURL];
[self.cachedSystemSounds setWithKey:cacheKey value:newSound];
return newSound.soundID;
}
#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
{
[LKStorage writeSyncWithBlock:^(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
{
// 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];
NSURL *_Nullable soundURL = [OWSSounds soundURLForSound:sound quiet:NO];
NSData *soundData = ^{
if (soundURL) {
return [NSData dataWithContentsOfURL:soundURL];
} else {
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) {
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
audioBehavior:(OWSAudioBehavior)audioBehavior;
{
NSURL *_Nullable soundURL = [OWSSounds soundURLForSound:sound quiet:NO];
if (!soundURL) {
return nil;
}
OWSAudioPlayer *player = [[OWSAudioPlayer alloc] initWithMediaUrl:soundURL audioBehavior:audioBehavior];
if ([self shouldAudioPlayerLoopForSound:sound]) {
player.isLooping = YES;
}
return player;
}
@end