From 4715240cc1deb39dc4846bee498ec7083333debd Mon Sep 17 00:00:00 2001 From: Christopher Kobusch Date: Tue, 15 Sep 2020 21:31:47 +0200 Subject: [PATCH] Added custom notification sounds * user can add custom notification sounds from Files app (aiff, wav, or caf files) * former OWSSound enum is now named OWSStandardSound, OWSSound turned into an alias for NSUInteger * custom sounds get IDs based on the hash of the file name --- .../OWSSoundSettingsViewController.h | 2 +- .../OWSSoundSettingsViewController.m | 30 +- .../ConversationViewController.m | 2 +- ...ationSettingsViewController+Contents.swift | 2 +- Signal/src/call/CallAudioService.swift | 16 +- .../translations/en.lproj/Localizable.strings | 3 + .../UserNotificationsAdaptee.swift | 4 +- SignalMessaging/environment/OWSSounds.h | 56 +-- SignalMessaging/environment/OWSSounds.m | 323 +++++++++++++----- .../migrations/OWS107LegacySounds.m | 4 +- 10 files changed, 323 insertions(+), 119 deletions(-) diff --git a/Signal/src/ViewControllers/AppSettings/OWSSoundSettingsViewController.h b/Signal/src/ViewControllers/AppSettings/OWSSoundSettingsViewController.h index 27b89e488d..3bf3f3e436 100644 --- a/Signal/src/ViewControllers/AppSettings/OWSSoundSettingsViewController.h +++ b/Signal/src/ViewControllers/AppSettings/OWSSoundSettingsViewController.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2020 Open Whisper Systems. All rights reserved. // #import "OWSTableViewController.h" diff --git a/Signal/src/ViewControllers/AppSettings/OWSSoundSettingsViewController.m b/Signal/src/ViewControllers/AppSettings/OWSSoundSettingsViewController.m index b71bfdfcb7..0db45045c1 100644 --- a/Signal/src/ViewControllers/AppSettings/OWSSoundSettingsViewController.m +++ b/Signal/src/ViewControllers/AppSettings/OWSSoundSettingsViewController.m @@ -12,7 +12,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface OWSSoundSettingsViewController () +@interface OWSSoundSettingsViewController () @property (nonatomic) BOOL isDirty; @@ -83,12 +83,12 @@ NS_ASSUME_NONNULL_BEGIN NSArray *allSounds = [OWSSounds allNotificationSounds]; for (NSNumber *nsValue in allSounds) { - OWSSound sound = (OWSSound)nsValue.intValue; + OWSSound sound = (OWSSound)nsValue.unsignedLongValue; OWSTableItem *item; NSString *soundLabelText = ^{ NSString *baseName = [OWSSounds displayNameForSound:sound]; - if (sound == OWSSound_Note) { + if (sound == OWSStandardSound_Note) { NSString *noteStringFormat = NSLocalizedString(@"SETTINGS_AUDIO_DEFAULT_TONE_LABEL_FORMAT", @"Format string for the default 'Note' sound. Embeds the system {{sound name}}."); return [NSString stringWithFormat:noteStringFormat, baseName]; @@ -115,6 +115,24 @@ NS_ASSUME_NONNULL_BEGIN [soundsSection addItem:item]; } + NSString *addCustomSoundItemTitle = NSLocalizedString(@"NOTIFICATIONS_SECTION_SOUNDS_ADD_CUSTOM_SOUND", + @"Label for settings UI that allows user to add a new notification sound."); + OWSTableItem *addCustomSoundItem = + [OWSTableItem disclosureItemWithText:addCustomSoundItemTitle + actionBlock:^{ + UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] + initWithDocumentTypes:@[ + @"com.microsoft.waveform-audio", + @"public.aifc-audio", + @"public.aiff-audio", + @"com.apple.coreaudio-format" + ] + inMode:UIDocumentPickerModeImport]; + picker.delegate = weakSelf; + [weakSelf presentViewController:picker animated:true completion:nil]; + }]; + [soundsSection addItem:addCustomSoundItem]; + [contents addSection:soundsSection]; self.contents = contents; @@ -159,6 +177,12 @@ NS_ASSUME_NONNULL_BEGIN [self.navigationController popViewControllerAnimated:YES]; } +- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls +{ + [OWSSounds importSoundsAtURLs:urls]; + [self updateTableContents]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index e102e2f33c..df0753028c 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -3112,7 +3112,7 @@ typedef enum : NSUInteger { self.inputToolbar.quotedReply = nil; if ([Environment.shared.preferences soundInForeground]) { - SystemSoundID soundId = [OWSSounds systemSoundIDForSound:OWSSound_MessageSent quiet:YES]; + SystemSoundID soundId = [OWSSounds systemSoundIDForSound:OWSStandardSound_MessageSent quiet:YES]; AudioServicesPlaySystemSound(soundId); } [self.typingIndicators didSendOutgoingMessageInThread:self.thread]; diff --git a/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift b/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift index 2ca558692e..033149078a 100644 --- a/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift +++ b/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift @@ -366,7 +366,7 @@ extension ConversationSettingsViewController { let cell = OWSTableItem.buildCellWithAccessoryLabel(icon: .settingsMessageSound, itemName: NSLocalizedString("SETTINGS_ITEM_NOTIFICATION_SOUND", comment: "Label for settings view that allows user to change the notification sound."), - accessoryText: OWSSounds.displayName(for: sound)) + accessoryText: OWSSounds.displayName(forSound: sound)) cell.accessibilityIdentifier = UIView.accessibilityIdentifier(in: self, name: "notifications") return cell }, diff --git a/Signal/src/call/CallAudioService.swift b/Signal/src/call/CallAudioService.swift index b22eb9d156..f25183b976 100644 --- a/Signal/src/call/CallAudioService.swift +++ b/Signal/src/call/CallAudioService.swift @@ -203,7 +203,7 @@ protocol CallAudioServiceDelegate: class { // HACK: Without this async, dialing sound only plays once. I don't really understand why. Does the audioSession // need some time to settle? Is somethign else interrupting our session? DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) { - self.play(sound: OWSSound.callConnecting) + self.play(sound: .callConnecting) } } @@ -216,7 +216,7 @@ protocol CallAudioServiceDelegate: class { AssertIsOnMainThread() Logger.debug("") - self.play(sound: OWSSound.callOutboundRinging) + self.play(sound: .callOutboundRinging) } private func handleLocalRinging(call: SignalCall) { @@ -266,7 +266,7 @@ protocol CallAudioServiceDelegate: class { AssertIsOnMainThread() Logger.debug("") - play(sound: OWSSound.callBusy) + play(sound: .callBusy) // Let the busy sound play for 4 seconds. The full file is longer than necessary DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 4.0) { @@ -310,12 +310,12 @@ protocol CallAudioServiceDelegate: class { stopRinging() } - private func prepareToPlay(sound: OWSSound) -> OWSAudioPlayer? { - guard let newPlayer = OWSSounds.audioPlayer(for: sound, audioBehavior: .call) else { - owsFailDebug("unable to build player for sound: \(OWSSounds.displayName(for: sound))") + private func prepareToPlay(sound: OWSStandardSound) -> OWSAudioPlayer? { + guard let newPlayer = OWSSounds.audioPlayer(forSound: sound.rawValue, audioBehavior: .call) else { + owsFailDebug("unable to build player for sound: \(OWSSounds.displayName(forSound: sound.rawValue))") return nil } - Logger.info("playing sound: \(OWSSounds.displayName(for: sound))") + Logger.info("playing sound: \(OWSSounds.displayName(forSound: sound.rawValue))") // It's important to stop the current player **before** starting the new player. In the case that // we're playing the same sound, since the player is memoized on the sound instance, we'd otherwise @@ -326,7 +326,7 @@ protocol CallAudioServiceDelegate: class { return newPlayer } - private func play(sound: OWSSound) { + private func play(sound: OWSStandardSound) { guard let newPlayer = prepareToPlay(sound: sound) else { return } newPlayer.play() } diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index bc33a4757e..e3779fbb51 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -2716,6 +2716,9 @@ /* Label for settings UI that allows user to change the notification sound. */ "NOTIFICATIONS_SECTION_SOUNDS" = "Sounds"; +/* Label for settings UI that allows user to add a new notification sound. */ +"NOTIFICATIONS_SECTION_SOUNDS_ADD_CUSTOM_SOUND" = "Add custom sound…"; + /* No comment provided by engineer. */ "NOTIFICATIONS_SENDER_AND_MESSAGE" = "Name, Content, and Actions"; diff --git a/SignalMessaging/Notifications/UserNotificationsAdaptee.swift b/SignalMessaging/Notifications/UserNotificationsAdaptee.swift index 3350e0660d..ec73f4391f 100644 --- a/SignalMessaging/Notifications/UserNotificationsAdaptee.swift +++ b/SignalMessaging/Notifications/UserNotificationsAdaptee.swift @@ -125,7 +125,7 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee { content.categoryIdentifier = category.identifier content.userInfo = userInfo let isAppActive = CurrentAppContext().isMainAppAndActive - if let sound = sound, sound != OWSSound.none { + if let sound = sound, sound != OWSStandardSound.none.rawValue { content.sound = sound.notificationSound(isQuiet: isAppActive) } @@ -316,7 +316,7 @@ public protocol ConversationSplit { extension OWSSound { func notificationSound(isQuiet: Bool) -> UNNotificationSound { - guard let filename = OWSSounds.filename(for: self, quiet: isQuiet) else { + guard let filename = OWSSounds.filename(forSound: 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 515e4b19b4..3cff0c00e8 100644 --- a/SignalMessaging/environment/OWSSounds.h +++ b/SignalMessaging/environment/OWSSounds.h @@ -7,38 +7,43 @@ NS_ASSUME_NONNULL_BEGIN -typedef NS_ENUM(NSUInteger, OWSSound) { - OWSSound_Default = 0, +typedef NSUInteger OWSSound; + +typedef NS_ENUM(NSUInteger, OWSStandardSound) { + OWSStandardSound_Default = 0, // Notification Sounds - OWSSound_Aurora, - OWSSound_Bamboo, - OWSSound_Chord, - OWSSound_Circles, - OWSSound_Complete, - OWSSound_Hello, - OWSSound_Input, - OWSSound_Keys, - OWSSound_Note, - OWSSound_Popcorn, - OWSSound_Pulse, - OWSSound_Synth, - OWSSound_SignalClassic, + OWSStandardSound_Aurora, + OWSStandardSound_Bamboo, + OWSStandardSound_Chord, + OWSStandardSound_Circles, + OWSStandardSound_Complete, + OWSStandardSound_Hello, + OWSStandardSound_Input, + OWSStandardSound_Keys, + OWSStandardSound_Note, + OWSStandardSound_Popcorn, + OWSStandardSound_Pulse, + OWSStandardSound_Synth, + OWSStandardSound_SignalClassic, // Ringtone Sounds - OWSSound_Reflection, + OWSStandardSound_Reflection, // Calls - OWSSound_CallConnecting, - OWSSound_CallOutboundRinging, - OWSSound_CallBusy, - OWSSound_CallEnded, + OWSStandardSound_CallConnecting, + OWSStandardSound_CallOutboundRinging, + OWSStandardSound_CallBusy, + OWSStandardSound_CallEnded, // Other - OWSSound_MessageSent, - OWSSound_None, - OWSSound_Silence, - OWSSound_DefaultiOSIncomingRingtone = OWSSound_Reflection, + OWSStandardSound_MessageSent, + OWSStandardSound_None, + OWSStandardSound_Silence, + OWSStandardSound_DefaultiOSIncomingRingtone = OWSStandardSound_Reflection, + + // Custom sound IDs begin at this threshold + OWSStandardSound_CustomThreshold = 1 << 16, // 16 == OWSCustomSoundShift }; @class OWSAudioPlayer; @@ -55,6 +60,9 @@ typedef NS_ENUM(NSUInteger, OWSSound) { + (nullable NSString *)filenameForSound:(OWSSound)sound; + (nullable NSString *)filenameForSound:(OWSSound)sound quiet:(BOOL)quiet; ++ (void)importSoundsAtURLs:(NSArray *)urls; ++ (NSString *)soundsDirectory; + #pragma mark - Notifications + (NSArray *)allNotificationSounds; diff --git a/SignalMessaging/environment/OWSSounds.m b/SignalMessaging/environment/OWSSounds.m index 421f23b4b5..4f516d8fc4 100644 --- a/SignalMessaging/environment/OWSSounds.m +++ b/SignalMessaging/environment/OWSSounds.m @@ -5,12 +5,17 @@ #import "OWSSounds.h" #import "Environment.h" #import "OWSAudioPlayer.h" +#import #import #import #import #import NSString *const kOWSSoundsStorageGlobalNotificationKey = @"kOWSSoundsStorageGlobalNotificationKey"; +// This name is specified in the payload by the Signal Service when requesting fallback push notifications. +NSString *const kDefaultNotificationSoundFilename = @"NewMessage.aifc"; + +const NSUInteger OWSCustomSoundShift = 16; @interface OWSSystemSound : NSObject @@ -100,9 +105,43 @@ NSString *const kOWSSoundsStorageGlobalNotificationKey = @"kOWSSoundsStorageGlob OWSSingletonAssert(); + [AppReadiness runNowOrWhenAppDidBecomeReadyPolite:^{ [OWSSounds cleanupOrphanedSounds]; }]; + return self; } ++ (void)cleanupOrphanedSounds +{ + NSSet *allCustomSounds = [NSSet setWithArray:[self allCustomNotificationSounds]]; + if (allCustomSounds.count == 0) { + return; + } + + __block NSSet *allInUseSounds; + [self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) { + allInUseSounds = [NSSet setWithArray:[self.keyValueStore allValuesWithTransaction:transaction]]; + }]; + + NSMutableSet *orphanedSounds = [allCustomSounds mutableCopy]; + [orphanedSounds minusSet:allInUseSounds]; + + if (orphanedSounds.count == 0) { + return; + } + + NSUInteger deletedSoundCount = 0; + for (NSNumber *soundNumber in orphanedSounds) { + OWSSound sound = soundNumber.unsignedLongValue; + if ([self deleteCustomSound:sound]) { + deletedSoundCount++; + } else { + OWSFailDebug(@"Failed to delete orphaned sound."); + } + } + + OWSLogInfo(@"Cleaned up %lu orphaned custom sounds.", deletedSoundCount); +} + + (SDSKeyValueStore *)keyValueStore { NSString *const kOWSSoundsStorageNotificationCollection = @"kOWSSoundsStorageNotificationCollection"; @@ -111,83 +150,87 @@ NSString *const kOWSSoundsStorageGlobalNotificationKey = @"kOWSSoundsStorageGlob + (NSArray *)allNotificationSounds { - return @[ + return [@[ // None and Note (default) should be first. - @(OWSSound_None), - @(OWSSound_Note), + @(OWSStandardSound_None), + @(OWSStandardSound_Note), - @(OWSSound_Aurora), - @(OWSSound_Bamboo), - @(OWSSound_Chord), - @(OWSSound_Circles), - @(OWSSound_Complete), - @(OWSSound_Hello), - @(OWSSound_Input), - @(OWSSound_Keys), - @(OWSSound_Popcorn), - @(OWSSound_Pulse), - @(OWSSound_SignalClassic), - @(OWSSound_Synth), - ]; + @(OWSStandardSound_Aurora), + @(OWSStandardSound_Bamboo), + @(OWSStandardSound_Chord), + @(OWSStandardSound_Circles), + @(OWSStandardSound_Complete), + @(OWSStandardSound_Hello), + @(OWSStandardSound_Input), + @(OWSStandardSound_Keys), + @(OWSStandardSound_Popcorn), + @(OWSStandardSound_Pulse), + @(OWSStandardSound_SignalClassic), + @(OWSStandardSound_Synth), + ] arrayByAddingObjectsFromArray:[OWSSounds allCustomNotificationSounds]]; } + (NSString *)displayNameForSound:(OWSSound)sound { // TODO: Should we localize these sound names? switch (sound) { - case OWSSound_Default: + case OWSStandardSound_Default: OWSFailDebug(@"invalid argument."); return @""; // Notification Sounds - case OWSSound_Aurora: + case OWSStandardSound_Aurora: return @"Aurora"; - case OWSSound_Bamboo: + case OWSStandardSound_Bamboo: return @"Bamboo"; - case OWSSound_Chord: + case OWSStandardSound_Chord: return @"Chord"; - case OWSSound_Circles: + case OWSStandardSound_Circles: return @"Circles"; - case OWSSound_Complete: + case OWSStandardSound_Complete: return @"Complete"; - case OWSSound_Hello: + case OWSStandardSound_Hello: return @"Hello"; - case OWSSound_Input: + case OWSStandardSound_Input: return @"Input"; - case OWSSound_Keys: + case OWSStandardSound_Keys: return @"Keys"; - case OWSSound_Note: + case OWSStandardSound_Note: return @"Note"; - case OWSSound_Popcorn: + case OWSStandardSound_Popcorn: return @"Popcorn"; - case OWSSound_Pulse: + case OWSStandardSound_Pulse: return @"Pulse"; - case OWSSound_Synth: + case OWSStandardSound_Synth: return @"Synth"; - case OWSSound_SignalClassic: + case OWSStandardSound_SignalClassic: return @"Signal Classic"; // Call Audio - case OWSSound_Reflection: + case OWSStandardSound_Reflection: return @"Opening"; - case OWSSound_CallConnecting: + case OWSStandardSound_CallConnecting: return @"Call Connecting"; - case OWSSound_CallOutboundRinging: + case OWSStandardSound_CallOutboundRinging: return @"Call Outboung Ringing"; - case OWSSound_CallBusy: + case OWSStandardSound_CallBusy: return @"Call Busy"; - case OWSSound_CallEnded: + case OWSStandardSound_CallEnded: return @"Call Ended"; - case OWSSound_MessageSent: + case OWSStandardSound_MessageSent: return @"Message Sent"; - case OWSSound_Silence: + case OWSStandardSound_Silence: return @"Silence"; // Other - case OWSSound_None: + case OWSStandardSound_None: return NSLocalizedString(@"SOUNDS_NONE", @"Label for the 'no sound' option that allows users to disable sounds for notifications, " @"etc."); + + // Custom Sounds + default: + return [OWSSounds displayNameForCustomSound:sound]; } } @@ -199,72 +242,80 @@ NSString *const kOWSSoundsStorageGlobalNotificationKey = @"kOWSSoundsStorageGlob + (nullable NSString *)filenameForSound:(OWSSound)sound quiet:(BOOL)quiet { switch (sound) { - case OWSSound_Default: + case OWSStandardSound_Default: OWSFailDebug(@"invalid argument."); return @""; // Notification Sounds - case OWSSound_Aurora: + case OWSStandardSound_Aurora: return (quiet ? @"aurora-quiet.aifc" : @"aurora.aifc"); - case OWSSound_Bamboo: + case OWSStandardSound_Bamboo: return (quiet ? @"bamboo-quiet.aifc" : @"bamboo.aifc"); - case OWSSound_Chord: + case OWSStandardSound_Chord: return (quiet ? @"chord-quiet.aifc" : @"chord.aifc"); - case OWSSound_Circles: + case OWSStandardSound_Circles: return (quiet ? @"circles-quiet.aifc" : @"circles.aifc"); - case OWSSound_Complete: + case OWSStandardSound_Complete: return (quiet ? @"complete-quiet.aifc" : @"complete.aifc"); - case OWSSound_Hello: + case OWSStandardSound_Hello: return (quiet ? @"hello-quiet.aifc" : @"hello.aifc"); - case OWSSound_Input: + case OWSStandardSound_Input: return (quiet ? @"input-quiet.aifc" : @"input.aifc"); - case OWSSound_Keys: + case OWSStandardSound_Keys: return (quiet ? @"keys-quiet.aifc" : @"keys.aifc"); - case OWSSound_Note: + case OWSStandardSound_Note: return (quiet ? @"note-quiet.aifc" : @"note.aifc"); - case OWSSound_Popcorn: + case OWSStandardSound_Popcorn: return (quiet ? @"popcorn-quiet.aifc" : @"popcorn.aifc"); - case OWSSound_Pulse: + case OWSStandardSound_Pulse: return (quiet ? @"pulse-quiet.aifc" : @"pulse.aifc"); - case OWSSound_Synth: + case OWSStandardSound_Synth: return (quiet ? @"synth-quiet.aifc" : @"synth.aifc"); - case OWSSound_SignalClassic: + case OWSStandardSound_SignalClassic: return (quiet ? @"classic-quiet.aifc" : @"classic.aifc"); // Ringtone Sounds - case OWSSound_Reflection: + case OWSStandardSound_Reflection: return @"Reflection.m4r"; // Calls - case OWSSound_CallConnecting: + case OWSStandardSound_CallConnecting: return @"ringback_tone_ansi.caf"; - case OWSSound_CallOutboundRinging: + case OWSStandardSound_CallOutboundRinging: return @"ringback_tone_ansi.caf"; - case OWSSound_CallBusy: + case OWSStandardSound_CallBusy: return @"busy_tone_ansi.caf"; - case OWSSound_CallEnded: + case OWSStandardSound_CallEnded: return @"end_call_tone_cept.caf"; - case OWSSound_MessageSent: + case OWSStandardSound_MessageSent: return @"message_sent.aiff"; - case OWSSound_Silence: + case OWSStandardSound_Silence: return @"silence.aiff"; // Other - case OWSSound_None: + case OWSStandardSound_None: return nil; + + // Custom Sounds + default: + return [OWSSounds filenameForCustomSound:sound]; } } + (nullable NSURL *)soundURLForSound:(OWSSound)sound quiet:(BOOL)quiet { - NSString *_Nullable filename = [self filenameForSound:sound quiet:quiet]; - if (!filename) { - return nil; + if (sound < OWSStandardSound_CustomThreshold) { + NSString *_Nullable filename = [self filenameForSound:sound quiet:quiet]; + if (!filename) { + return nil; + } + NSURL *_Nullable url = [[NSBundle mainBundle] URLForResource:filename.stringByDeletingPathExtension + withExtension:filename.pathExtension]; + OWSAssertDebug(url); + return url; + } else { + return [OWSSounds soundURLForCustomSound:sound]; } - NSURL *_Nullable url = [[NSBundle mainBundle] URLForResource:filename.stringByDeletingPathExtension - withExtension:filename.pathExtension]; - OWSAssertDebug(url); - return url; } + (SystemSoundID)systemSoundIDForSound:(OWSSound)sound quiet:(BOOL)quiet @@ -289,11 +340,49 @@ NSString *const kOWSSoundsStorageGlobalNotificationKey = @"kOWSSoundsStorageGlob return newSound.soundID; } ++ (void)importSoundsAtURLs:(NSArray *)urls +{ + NSFileManager *fileManager = [NSFileManager defaultManager]; + for (NSURL *url in urls) { + NSError *error = NULL; + NSString *filename = url.lastPathComponent; + if (!filename) + continue; + + NSString *destination = [NSString pathWithComponents:@[ [OWSSounds soundsDirectory], filename ]]; + + if (![fileManager fileExistsAtPath:destination]) { + [fileManager copyItemAtPath:[url path] toPath:destination error:&error]; + } + + if (error) { + OWSFailDebug(@"Failed to import custom sound with error %@", [error localizedDescription]); + error = NULL; + } + } +} + ++ (BOOL)deleteCustomSound:(OWSSound)sound +{ + if (sound < OWSStandardSound_CustomThreshold) { + OWSFailDebug(@"Can't delete built-in sound"); + return NO; + } else { + NSURL *url = [self soundURLForCustomSound:sound]; + NSError *error; + [OWSFileSystem deleteFileIfExistsWithUrl:url error:&error]; + if (error) { + OWSFailDebug(@"Failed to delete custom sound: %@", error); + } + return YES; + } +} + #pragma mark - Notifications + (OWSSound)defaultNotificationSound { - return OWSSound_Note; + return OWSStandardSound_Note; } + (OWSSound)globalNotificationSound @@ -303,7 +392,7 @@ NSString *const kOWSSoundsStorageGlobalNotificationKey = @"kOWSSoundsStorageGlob value = [self.keyValueStore getNSNumber:kOWSSoundsStorageGlobalNotificationKey transaction:transaction]; }]; // Default to the global default. - return (value ? (OWSSound)value.intValue : [self defaultNotificationSound]); + return (value ? (OWSSound)value.unsignedLongValue : [self defaultNotificationSound]); } + (void)setGlobalNotificationSound:(OWSSound)sound @@ -332,11 +421,8 @@ NSString *const kOWSSoundsStorageGlobalNotificationKey = @"kOWSSoundsStorageGlob // 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]; + NSString *dirPath = [OWSSounds soundsDirectory]; - // 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]; OWSLogDebug(@"writing new default sound to %@", defaultSoundPath); @@ -347,7 +433,7 @@ NSString *const kOWSSoundsStorageGlobalNotificationKey = @"kOWSSoundsStorageGlob if (soundURL) { return [NSData dataWithContentsOfURL:soundURL]; } else { - OWSAssertDebug(sound == OWSSound_None); + OWSAssertDebug(sound == OWSStandardSound_None); return [NSData new]; } }(); @@ -375,7 +461,7 @@ NSString *const kOWSSoundsStorageGlobalNotificationKey = @"kOWSSoundsStorageGlob value = [self.keyValueStore getNSNumber:thread.uniqueId transaction:transaction]; }]; // Default to the "global" notification sound, which in turn will default to the global default. - return (value ? (OWSSound)value.intValue : [self globalNotificationSound]); + return (value ? (OWSSound)value.unsignedLongValue : [self globalNotificationSound]); } + (void)setNotificationSound:(OWSSound)sound forThread:(TSThread *)thread @@ -385,12 +471,95 @@ NSString *const kOWSSoundsStorageGlobalNotificationKey = @"kOWSSoundsStorageGlob }); } +#pragma mark - Custom Sounds + ++ (NSString *)displayNameForCustomSound:(OWSSound)sound +{ + NSString *filename = [OWSSounds filenameForCustomSound:sound]; + NSString *fileNameWithoutExtension = [[filename lastPathComponent] stringByDeletingPathExtension]; + if (!fileNameWithoutExtension) { + OWSFailDebug(@"Unable to retrieve custom sound display name from: %lu", sound); + return @"Custom Sound"; + } + + return [fileNameWithoutExtension capitalizedString]; +} + ++ (nullable NSString *)filenameForCustomSound:(OWSSound)sound +{ + NSError *error = NULL; + NSArray *customSoundFilenames = + [[NSFileManager defaultManager] contentsOfDirectoryAtPath:[OWSSounds soundsDirectory] error:&error]; + if (error) { + OWSFailDebug(@"Failed retrieving custom sound files: %@", error.localizedDescription); + return NULL; + } + + NSUInteger index = + [customSoundFilenames indexOfObjectPassingTest:^BOOL(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { + return [OWSSounds customSoundForFilename:obj] == sound; + }]; + return index == NSNotFound ? NULL : [customSoundFilenames objectAtIndex:index]; +} + ++ (NSArray *)allCustomNotificationSounds +{ + NSError *error = NULL; + NSArray *customSoundFilenames = + [[NSFileManager defaultManager] contentsOfDirectoryAtPath:[OWSSounds soundsDirectory] error:&error]; + if (error) { + OWSFailDebug(@"Failed retrieving custom sound files: %@", error.localizedDescription); + return @[]; + } + + NSMutableArray *sounds = [[NSMutableArray alloc] initWithCapacity:customSoundFilenames.count]; + for (NSString *filename in customSoundFilenames) { + if ([filename isEqualToString:kDefaultNotificationSoundFilename]) { + continue; + } + [sounds addObject:[NSNumber numberWithUnsignedLong:[OWSSounds customSoundForFilename:filename]]]; + } + return sounds; +} + ++ (nullable NSURL *)soundURLForCustomSound:(OWSSound)sound +{ + NSString *path = + [[OWSSounds soundsDirectory] stringByAppendingPathComponent:[OWSSounds filenameForCustomSound:sound]]; + return [NSURL fileURLWithPath:path]; +} + ++ (OWSSound)customSoundForFilename:(NSString *)filename +{ + NSUInteger hashValue = 0; + NSData *_Nullable filenameData = [filename dataUsingEncoding:NSUTF8StringEncoding]; + if (!filenameData) { + OWSFailDebug(@"could not get data from filename."); + return OWSStandardSound_Default; + } + NSData *_Nullable hashData = [Cryptography computeSHA256Digest:filenameData truncatedToBytes:sizeof(hashValue)]; + if (!hashData) { + OWSFailDebug(@"could not get hash from filename."); + return OWSStandardSound_Default; + } + [hashData getBytes:&hashValue length:sizeof(hashValue)]; + + return hashValue << OWSCustomSoundShift; +} + ++ (NSString *)soundsDirectory +{ + NSString *directory = [[OWSFileSystem appLibraryDirectoryPath] stringByAppendingPathComponent:@"Sounds"]; + [OWSFileSystem ensureDirectoryExists:directory]; + return directory; +} + #pragma mark - AudioPlayer + (BOOL)shouldAudioPlayerLoopForSound:(OWSSound)sound { - return (sound == OWSSound_CallConnecting || sound == OWSSound_CallOutboundRinging - || sound == OWSSound_DefaultiOSIncomingRingtone); + return (sound == OWSStandardSound_CallConnecting || sound == OWSStandardSound_CallOutboundRinging + || sound == OWSStandardSound_DefaultiOSIncomingRingtone); } + (nullable OWSAudioPlayer *)audioPlayerForSound:(OWSSound)sound audioBehavior:(OWSAudioBehavior)audioBehavior diff --git a/SignalMessaging/environment/migrations/OWS107LegacySounds.m b/SignalMessaging/environment/migrations/OWS107LegacySounds.m index 232a5234f1..13e27f134a 100644 --- a/SignalMessaging/environment/migrations/OWS107LegacySounds.m +++ b/SignalMessaging/environment/migrations/OWS107LegacySounds.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// Copyright (c) 2020 Open Whisper Systems. All rights reserved. // #import "OWS107LegacySounds.h" @@ -23,7 +23,7 @@ static NSString *const OWS107LegacySoundsMigrationId = @"107"; { OWSAssertDebug(transaction); - [OWSSounds setGlobalNotificationSound:OWSSound_SignalClassic transaction:transaction.asAnyWrite]; + [OWSSounds setGlobalNotificationSound:OWSStandardSound_SignalClassic transaction:transaction.asAnyWrite]; } @end