From d9d1e0ec186f1bbd09816af25e022bb7768197bb Mon Sep 17 00:00:00 2001 From: guodongmao Date: Wed, 3 Apr 2024 01:59:48 -0700 Subject: [PATCH] Finished Allow custom sounds per sender #991 --- Monal/AlertSounds/{alert9.aif => Area 51.aif} | Bin Monal/AlertSounds/{alert4.aif => Bing.aif} | Bin Monal/AlertSounds/{alert3.aif => Bloop.aif} | Bin Monal/AlertSounds/{alert11.aif => Chirp.aif} | Bin Monal/AlertSounds/{alert8.aif => Echo.aif} | Bin Monal/AlertSounds/{alert7.aif => Forest.aif} | Bin Monal/AlertSounds/{alert1.aif => Morse.aif} | Bin Monal/AlertSounds/{alert5.aif => Pipa.aif} | Bin Monal/AlertSounds/{alert12.aif => Sonar.aif} | Bin Monal/AlertSounds/{alert6.aif => Water.aif} | Bin Monal/AlertSounds/{alert10.aif => Wood.aif} | Bin .../AlertSounds/{alert2.aif => Xylophone.aif} | Bin Monal/Classes/ContactDetails.swift | 4 + Monal/Classes/DataLayer.h | 11 +- Monal/Classes/DataLayer.m | 202 ++++++++++++++++++ Monal/Classes/MLNotificationManager.h | 1 + Monal/Classes/MLNotificationManager.m | 36 ++-- Monal/Classes/MLSettingsTableViewController.m | 8 +- Monal/Classes/MLSoundManager.h | 31 +++ Monal/Classes/MLSoundManager.m | 115 ++++++++++ Monal/Classes/SoundPickerView.swift | 147 +++++++++++++ Monal/Classes/SoundsSettingView.swift | 185 ++++++++++++++++ Monal/Classes/SwiftuiHelpers.swift | 10 + Monal/monalxmpp/monalxmpp.h | 1 + 24 files changed, 729 insertions(+), 22 deletions(-) rename Monal/AlertSounds/{alert9.aif => Area 51.aif} (100%) rename Monal/AlertSounds/{alert4.aif => Bing.aif} (100%) rename Monal/AlertSounds/{alert3.aif => Bloop.aif} (100%) rename Monal/AlertSounds/{alert11.aif => Chirp.aif} (100%) rename Monal/AlertSounds/{alert8.aif => Echo.aif} (100%) rename Monal/AlertSounds/{alert7.aif => Forest.aif} (100%) rename Monal/AlertSounds/{alert1.aif => Morse.aif} (100%) rename Monal/AlertSounds/{alert5.aif => Pipa.aif} (100%) rename Monal/AlertSounds/{alert12.aif => Sonar.aif} (100%) rename Monal/AlertSounds/{alert6.aif => Water.aif} (100%) rename Monal/AlertSounds/{alert10.aif => Wood.aif} (100%) rename Monal/AlertSounds/{alert2.aif => Xylophone.aif} (100%) create mode 100644 Monal/Classes/MLSoundManager.h create mode 100644 Monal/Classes/MLSoundManager.m create mode 100644 Monal/Classes/SoundPickerView.swift create mode 100644 Monal/Classes/SoundsSettingView.swift diff --git a/Monal/AlertSounds/alert9.aif b/Monal/AlertSounds/Area 51.aif similarity index 100% rename from Monal/AlertSounds/alert9.aif rename to Monal/AlertSounds/Area 51.aif diff --git a/Monal/AlertSounds/alert4.aif b/Monal/AlertSounds/Bing.aif similarity index 100% rename from Monal/AlertSounds/alert4.aif rename to Monal/AlertSounds/Bing.aif diff --git a/Monal/AlertSounds/alert3.aif b/Monal/AlertSounds/Bloop.aif similarity index 100% rename from Monal/AlertSounds/alert3.aif rename to Monal/AlertSounds/Bloop.aif diff --git a/Monal/AlertSounds/alert11.aif b/Monal/AlertSounds/Chirp.aif similarity index 100% rename from Monal/AlertSounds/alert11.aif rename to Monal/AlertSounds/Chirp.aif diff --git a/Monal/AlertSounds/alert8.aif b/Monal/AlertSounds/Echo.aif similarity index 100% rename from Monal/AlertSounds/alert8.aif rename to Monal/AlertSounds/Echo.aif diff --git a/Monal/AlertSounds/alert7.aif b/Monal/AlertSounds/Forest.aif similarity index 100% rename from Monal/AlertSounds/alert7.aif rename to Monal/AlertSounds/Forest.aif diff --git a/Monal/AlertSounds/alert1.aif b/Monal/AlertSounds/Morse.aif similarity index 100% rename from Monal/AlertSounds/alert1.aif rename to Monal/AlertSounds/Morse.aif diff --git a/Monal/AlertSounds/alert5.aif b/Monal/AlertSounds/Pipa.aif similarity index 100% rename from Monal/AlertSounds/alert5.aif rename to Monal/AlertSounds/Pipa.aif diff --git a/Monal/AlertSounds/alert12.aif b/Monal/AlertSounds/Sonar.aif similarity index 100% rename from Monal/AlertSounds/alert12.aif rename to Monal/AlertSounds/Sonar.aif diff --git a/Monal/AlertSounds/alert6.aif b/Monal/AlertSounds/Water.aif similarity index 100% rename from Monal/AlertSounds/alert6.aif rename to Monal/AlertSounds/Water.aif diff --git a/Monal/AlertSounds/alert10.aif b/Monal/AlertSounds/Wood.aif similarity index 100% rename from Monal/AlertSounds/alert10.aif rename to Monal/AlertSounds/Wood.aif diff --git a/Monal/AlertSounds/alert2.aif b/Monal/AlertSounds/Xylophone.aif similarity index 100% rename from Monal/AlertSounds/alert2.aif rename to Monal/AlertSounds/Xylophone.aif diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index 1f3af26879..86207b1c3f 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -190,6 +190,10 @@ struct ContactDetails: View { NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:contact, delegate:delegate))) { Text("Change Chat Background") } + + NavigationLink(destination: LazyClosureView(SoundsSettingView(contact:contact, delegate:delegate))) { + Text("Sounds") + } } .listStyle(.plain) diff --git a/Monal/Classes/DataLayer.h b/Monal/Classes/DataLayer.h index b030a79a5f..13a3e5535c 100644 --- a/Monal/Classes/DataLayer.h +++ b/Monal/Classes/DataLayer.h @@ -94,7 +94,7 @@ extern NSString* const kMessageTypeFiletransfer; -(BOOL) hasContactRequestForContact:(MLContact*) contact; -(NSMutableArray*) allContactRequests; -(void) addContactRequest:(MLContact *) requestor; --(void) deleteContactRequest:(MLContact *) requestor; +-(void) deleteContactRequest:(MLContact *) requestor; #pragma mark Contact info @@ -106,6 +106,15 @@ extern NSString* const kMessageTypeFiletransfer; -(BOOL) saveMessageDraft:(NSString*) buddy forAccount:(NSNumber*) accountNo withComment:(NSString*) comment; -(NSString*) loadMessageDraft:(NSString*) buddy forAccount:(NSNumber*) accountNo; +#pragma mark - sound +-(void) setAlertSoundWithAccountId:(NSString*) accountId buddyId:(NSString*) buddyId soundName:(NSString*) soundName soundData:(NSData*) soundData isCustom:(NSNumber*) custom; +-(NSData*) getSoundDataForAccountId:(NSString*) accountId buddyId:(NSString*) buddyId; +-(NSString*) getSoundNameForAccountId:(NSString*) accountId buddyId:(NSString*) buddyId; +-(NSNumber*) getIsCustomSoundForAccountId:(NSString*) accountId buddyId:(NSString*) buddyId; +-(void) checkAndCreateAlertSoundsTable; +-(void) deleteSoundForAccountId:(NSString*) accountId buddyId:(NSString*) buddyId; +-(void) deleteSoundsForBuddyId:(NSString *)buddyId; + #pragma mark - MUC -(BOOL) initMuc:(NSString*) room forAccountId:(NSNumber*) accountNo andMucNick:(NSString* _Nullable) mucNick; diff --git a/Monal/Classes/DataLayer.m b/Monal/Classes/DataLayer.m index 12ec768ee9..163953cc44 100644 --- a/Monal/Classes/DataLayer.m +++ b/Monal/Classes/DataLayer.m @@ -2522,4 +2522,206 @@ -(NSArray*) searchResultOfHistoryMessageWithKeyWords:(NSString*) keyword between }]; } +#pragma mark - sounds + +-(void) setAlertSoundWithAccountId:(NSString*) accountId buddyId:(NSString* )buddyId soundName:(NSString*) soundName soundData:(NSData*) soundData isCustom:(NSNumber*) custom +{ + if(!soundName || !soundData) + { + return; + } + BOOL isAccountIdDefault = [accountId isEqualToString:@"Default"]; + BOOL isBuddyIdGlobal = [buddyId isEqualToString:@"global"]; + + NSMutableArray* arguments = [NSMutableArray array]; + if(!isAccountIdDefault) + { + [arguments addObject:accountId]; + } + if(!isBuddyIdGlobal) + { + [arguments addObject:buddyId]; + } + + NSString* accountIdCondition = isAccountIdDefault ? @"account_id IS NULL" : @"account_id = ?"; + NSString* buddyIdCondition = isBuddyIdGlobal ? @"buddy_id IS NULL" : @"buddy_id = ?"; + + [self.db voidWriteTransaction:^{ + NSString* selectSql = [NSString stringWithFormat:@"SELECT COUNT(*) FROM alertsounds WHERE %@ AND %@;", accountIdCondition, buddyIdCondition]; + id countResult = [self.db executeScalar:selectSql andArguments:arguments]; + NSInteger count = [countResult integerValue]; + + if(count > 0) + { + NSString* whereClause = @""; + NSMutableArray* arguments = [NSMutableArray arrayWithObjects:soundName, soundData, custom, nil]; + + if ([accountId isEqualToString:@"Default"] && [buddyId isEqualToString:@"global"]) + { + whereClause = @"account_id IS NULL AND buddy_id IS NULL"; + } + else if([accountId isEqualToString:@"Default"]) + { + whereClause = @"account_id IS NULL AND buddy_id = ?"; + [arguments addObject:buddyId]; + } + else if([buddyId isEqualToString:@"global"]) + { + whereClause = @"account_id = ? AND buddy_id IS NULL"; + [arguments addObject:accountId]; + } + else + { + whereClause = @"account_id = ? AND buddy_id = ?"; + [arguments addObjectsFromArray:@[accountId, buddyId]]; + } + NSString* updateSql = [NSString stringWithFormat:@"UPDATE alertsounds SET soundname = ?, sounddata = ?, customsound = ? WHERE %@", whereClause]; + [self.db executeNonQuery:updateSql andArguments:arguments]; + } + else + { + NSString* insertSql = @"INSERT INTO alertsounds (account_id, buddy_id, soundname, sounddata, customsound) VALUES (?, ?, ?, ?, ?);"; + NSArray* insertArguments = @[isAccountIdDefault ? [NSNull null] : accountId, isBuddyIdGlobal ? [NSNull null] : buddyId, soundName, soundData, custom]; + [self.db executeNonQuery:insertSql andArguments:insertArguments]; + } + }]; +} + + +-(NSString*) getSoundNameForAccountId:(NSString*) accountId buddyId:(NSString*) buddyId +{ + return [self.db idReadTransaction:^id{ + NSString* accountIdCondition = [accountId isEqualToString:@"Default"] ? @"IS NULL" : @"= ?"; + NSString* buddyIdCondition = [buddyId isEqualToString:@"global"] ? @"IS NULL" : @"= ?"; + + NSString* query = [NSString stringWithFormat:@"SELECT soundname FROM alertsounds WHERE account_id %@ AND buddy_id %@ LIMIT 1;", accountIdCondition, buddyIdCondition]; + + NSMutableArray* arguments = [[NSMutableArray alloc] init]; + if (![accountId isEqualToString:@"Default"]) + { + [arguments addObject:accountId]; + } + if (![buddyId isEqualToString:@"global"]) + { + [arguments addObject:buddyId]; + } + id queryResult = [self.db executeScalar:query andArguments:arguments]; + NSString* soundName = queryResult ? [NSString stringWithFormat:@"%@", queryResult] : nil; + return soundName; + }]; +} + +-(NSData*) getSoundDataForAccountId:(NSString*) accountId buddyId:(NSString*) buddyId +{ + return [self.db idReadTransaction:^id{ + NSString* accountIdCondition = [accountId isEqualToString:@"Default"] ? @"IS NULL" : @"= ?"; + NSString* buddyIdCondition = [buddyId isEqualToString:@"global"] ? @"IS NULL" : @"= ?"; + + NSString* query = [NSString stringWithFormat:@"SELECT sounddata FROM alertsounds WHERE account_id %@ AND buddy_id %@ LIMIT 1;", accountIdCondition, buddyIdCondition]; + + NSMutableArray* arguments = [[NSMutableArray alloc] init]; + if(![accountId isEqualToString:@"Default"]) + { + [arguments addObject:accountId]; + } + if(![buddyId isEqualToString:@"global"]) + { + [arguments addObject:buddyId]; + } + + id queryResult = [self.db executeScalar:query andArguments:arguments]; + NSData* soundData = [NSData dataWithData:queryResult]; + return soundData; + }]; +} + +-(NSNumber*) getIsCustomSoundForAccountId:(NSString*) accountId buddyId:(NSString*) buddyId +{ + return [self.db idReadTransaction:^id{ + NSString* accountIdCondition = [accountId isEqualToString:@"Default"] ? @"IS NULL" : @"= ?"; + NSString* buddyIdCondition = [buddyId isEqualToString:@"global"] ? @"IS NULL" : @"= ?"; + + NSString* query = [NSString stringWithFormat:@"SELECT customsound FROM alertsounds WHERE account_id %@ AND buddy_id %@ LIMIT 1;", accountIdCondition, buddyIdCondition]; + + NSMutableArray* arguments = [[NSMutableArray alloc] init]; + if(![accountId isEqualToString:@"Default"]) + { + [arguments addObject:accountId]; + } + if(![buddyId isEqualToString:@"global"]) + { + [arguments addObject:buddyId]; + } + + id queryResult = [self.db executeScalar:query andArguments:arguments]; + NSNumber* customSound = queryResult; + return customSound; + }]; +} + +-(void) checkAndCreateAlertSoundsTable +{ + [self.db idReadTransaction:^id{ + NSString* checkTableExistsQuery = @"SELECT name FROM sqlite_master WHERE type='table' AND name='alertsounds';"; + id tableExistsResult = [self.db executeScalar:checkTableExistsQuery]; + + if(!tableExistsResult) + { + NSString* createTableQuery = @"CREATE TABLE alertsounds (alertsound_id INTEGER PRIMARY KEY, account_id VARCHAR, buddy_id VARCHAR, soundname VARCHAR, sounddata BLOB, customsound INTEGER);"; + + [self.db executeNonQuery:createTableQuery]; + + NSLog(@"alertsounds table created."); + } + else + { + NSLog(@"alertsounds table already exists."); + } + return nil; + }]; +} + + +-(void) deleteSoundForAccountId:(NSString *) accountId buddyId:(NSString *) buddyId +{ + [self.db voidWriteTransaction:^{ + BOOL isAccountIdDefault = [accountId isEqualToString:@"Default"]; + BOOL isBuddyIdGlobal = [buddyId isEqualToString:@"global"]; + + NSMutableArray* arguments = [NSMutableArray array]; + NSString* query; + + if(isAccountIdDefault && isBuddyIdGlobal) + { + query = @"DELETE FROM alertsounds WHERE account_id IS NULL AND buddy_id IS NULL;"; + } + else if(isAccountIdDefault) + { + query = @"DELETE FROM alertsounds WHERE account_id IS NULL AND buddy_id = ?;"; + [arguments addObject:buddyId]; + } + else if(isBuddyIdGlobal) + { + query = @"DELETE FROM alertsounds WHERE account_id = ? AND buddy_id IS NULL;"; + [arguments addObject:accountId]; + } + else + { + query = @"DELETE FROM alertsounds WHERE account_id = ? AND buddy_id = ?;"; + [arguments addObject:accountId]; + [arguments addObject:buddyId]; + } + [self.db executeNonQuery:query andArguments:arguments]; + }]; +} + +-(void) deleteSoundsForBuddyId:(NSString *) buddyId +{ + [self.db voidWriteTransaction:^{ + NSString *query = @"DELETE FROM alertsounds WHERE buddy_id = ?;"; + NSArray *arguments = @[buddyId]; + [self.db executeNonQuery:query andArguments:arguments]; + }]; +} + @end diff --git a/Monal/Classes/MLNotificationManager.h b/Monal/Classes/MLNotificationManager.h index 6ebee284d4..1454799887 100644 --- a/Monal/Classes/MLNotificationManager.h +++ b/Monal/Classes/MLNotificationManager.h @@ -10,6 +10,7 @@ #import #import "MLConstants.h" #import "DataLayer.h" +#import "MLSoundManager.h" /** Singleton object that will handle all sliders, alerts and sounds. listens for new message notification. diff --git a/Monal/Classes/MLNotificationManager.m b/Monal/Classes/MLNotificationManager.m index 5497a5b4bf..8b79aedcc8 100644 --- a/Monal/Classes/MLNotificationManager.m +++ b/Monal/Classes/MLNotificationManager.m @@ -337,16 +337,16 @@ -(void) playNotificationSoundForMessage:(MLMessage*) message withSound:(BOOL) so if(sound && [[HelperTools defaultsDB] boolForKey:@"Sound"]) { - NSString* filename = [[HelperTools defaultsDB] objectForKey:@"AlertSoundFile"]; - if(filename) + NSString* senderJID = [message.buddyName lowercaseString]; + NSString* receiverJID = account.connectionProperties.identity.jid; + NSString* soundName = [[MLSoundManager sharedInstance] getSoundNameForSenderJID:senderJID andReceiverJID:receiverJID]; + if([soundName isEqualToString:@""]) { - content.sound = [UNNotificationSound soundNamed:[NSString stringWithFormat:@"AlertSounds/%@.aif", filename]]; - DDLogDebug(@"Using user configured alert sound: %@", content.sound); + content.sound = [UNNotificationSound defaultSound]; } else { - content.sound = [UNNotificationSound defaultSound]; - DDLogDebug(@"Using default alert sound: %@", content.sound); + content.sound = [UNNotificationSound soundNamed:soundName]; } } else @@ -470,16 +470,16 @@ -(void) showModernNotificationForMessage:(MLMessage*) message withSound:(BOOL) s if(sound && [[HelperTools defaultsDB] boolForKey:@"Sound"]) { - NSString* filename = [[HelperTools defaultsDB] objectForKey:@"AlertSoundFile"]; - if(filename) + NSString* senderJID = [message.buddyName lowercaseString]; + NSString* receiverJID = account.connectionProperties.identity.jid; + NSString* soundName = [[MLSoundManager sharedInstance] getSoundNameForSenderJID:senderJID andReceiverJID:receiverJID]; + if([soundName isEqualToString:@""]) { - content.sound = [UNNotificationSound soundNamed:[NSString stringWithFormat:@"AlertSounds/%@.aif", filename]]; - DDLogDebug(@"Using user configured alert sound: %@", content.sound); + content.sound = [UNNotificationSound defaultSound]; } else { - content.sound = [UNNotificationSound defaultSound]; - DDLogDebug(@"Using default alert sound: %@", content.sound); + content.sound = [UNNotificationSound soundNamed:soundName]; } } else @@ -732,16 +732,16 @@ -(void) showLegacyNotificationForMessage:(MLMessage*) message withSound:(BOOL) s if(sound && [[HelperTools defaultsDB] boolForKey:@"Sound"]) { - NSString* filename = [[HelperTools defaultsDB] objectForKey:@"AlertSoundFile"]; - if(filename) + NSString* senderJID = [message.buddyName lowercaseString]; + NSString* receiverJID = contact.contactJid; + NSString* soundName = [[MLSoundManager sharedInstance] getSoundNameForSenderJID:senderJID andReceiverJID:receiverJID]; + if([soundName isEqualToString:@""]) { - content.sound = [UNNotificationSound soundNamed:[NSString stringWithFormat:@"AlertSounds/%@.aif", filename]]; - DDLogDebug(@"Using user configured alert sound: %@", content.sound); + content.sound = [UNNotificationSound defaultSound]; } else { - content.sound = [UNNotificationSound defaultSound]; - DDLogDebug(@"Using default alert sound: %@", content.sound); + content.sound = [UNNotificationSound soundNamed:soundName]; } } else diff --git a/Monal/Classes/MLSettingsTableViewController.m b/Monal/Classes/MLSettingsTableViewController.m index e022cb5047..7da62f97f3 100644 --- a/Monal/Classes/MLSettingsTableViewController.m +++ b/Monal/Classes/MLSettingsTableViewController.m @@ -77,7 +77,7 @@ @interface MLSettingsTableViewController () { @end -@implementation MLSettingsTableViewController +@implementation MLSettingsTableViewController -(IBAction) close:(id) sender @@ -337,9 +337,11 @@ -(void)tableView:(UITableView*) tableView didSelectRowAtIndexPath:(NSIndexPath*) [self showDetailViewController:backgroundSettingsController sender:self]; break; } - case SoundsRow: - [self performSegueWithIdentifier:@"showSounds" sender:self]; + case SoundsRow: { + UIViewController* soundSettingsController =[[SwiftuiInterface new] makeSoundSettings:nil]; + [self showDetailViewController:soundSettingsController sender:self]; break; + } default: unreachable(); } diff --git a/Monal/Classes/MLSoundManager.h b/Monal/Classes/MLSoundManager.h new file mode 100644 index 0000000000..68d3f6e4f8 --- /dev/null +++ b/Monal/Classes/MLSoundManager.h @@ -0,0 +1,31 @@ +// +// MLSoundManager.h +// Monal +// +// Created by 阿栋 on 3/29/24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class MLContact; + +@interface MLSoundManager : NSObject + + +@property (nonatomic, strong) NSString* _Nullable selectedSound; + + ++ (MLSoundManager*) sharedInstance; +-(NSArray*) listBundledSounds; +-(NSData*) getSoundDataForSenderJID:(NSString*) senderJID andReceiverJID:(NSString*) receiverJID; +-(NSString*) getSoundNameForSenderJID:(NSString*) senderJID andReceiverJID:(NSString*) receiverJID; +-(void) saveSoundData:(NSData*) soundData forSenderJID:(NSString*) senderJID andReceiverJID:(NSString*) receiverJID WithSoundFileName:(NSString*) filename isCustomSound:(NSNumber*) isCustom; +-(NSNumber*) getIsCustomSoundForAccountId:(NSString*) accountId buddyId:(NSString*) buddyId; +-(void) deleteContactForAccountId:(NSString*) accountId; +@end + +NS_ASSUME_NONNULL_END + diff --git a/Monal/Classes/MLSoundManager.m b/Monal/Classes/MLSoundManager.m new file mode 100644 index 0000000000..e09b55d28f --- /dev/null +++ b/Monal/Classes/MLSoundManager.m @@ -0,0 +1,115 @@ +// +// MLSoundManager.m +// Monal +// +// Created by 阿栋 on 3/29/24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +#import "MLSoundManager.h" +#import "MLXMPPManager.h" +#import "HelperTools.h" +#import "DataLayer.h" + +@interface MLSoundManager() +@property (nonatomic, strong) NSString* documentsDirectory; +@end + +@implementation MLSoundManager + +#pragma mark initilization + ++(MLSoundManager*) sharedInstance +{ + static dispatch_once_t once; + static MLSoundManager* sharedInstance; + dispatch_once(&once, ^{ + DDLogVerbose(@"Creating shared sound manager instance..."); + sharedInstance = [MLSoundManager new]; + }); + return sharedInstance; +} + +-(id) init +{ + self = [super init]; + if(self) + { + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSURL* soundsDirectoryURL = [HelperTools getContainerURLForPathComponents:@[@"Library", @"Sounds"]]; + self.documentsDirectory = [soundsDirectoryURL path]; + [fileManager createDirectoryAtURL:soundsDirectoryURL withIntermediateDirectories:YES attributes:nil error:nil]; + [HelperTools configureFileProtectionFor:[soundsDirectoryURL path]]; + [self checkAndCreateAlertSoundsTable]; + } + return self; +} + +-(NSArray*) listBundledSounds +{ + NSString* resourcePath = [[NSBundle mainBundle] resourcePath]; + NSString* alertSoundsPath = [resourcePath stringByAppendingPathComponent:@"AlertSounds"]; + NSError* error; + NSArray* soundFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:alertSoundsPath error:&error]; + if(error) + { + DDLogError(@"Error listing files in directory: %@", error.localizedDescription); + } + NSMutableArray* sounds = [[NSMutableArray alloc] init]; + for(NSString* file in soundFiles) + { + NSString* fileNameWithoutExtension = [file stringByDeletingPathExtension]; + [sounds addObject:fileNameWithoutExtension]; + } + return [sounds copy]; +} + +- (NSString*) getSoundNameForSenderJID:(NSString*) senderJID andReceiverJID:(NSString*) receiverJID +{ + NSString *soundName = [[DataLayer sharedInstance] getSoundNameForAccountId:receiverJID buddyId:senderJID]; + if(soundName.length == 0) + { + soundName = [[DataLayer sharedInstance] getSoundNameForAccountId:@"Default" buddyId:senderJID]; + if(soundName.length == 0) + { + soundName = [[DataLayer sharedInstance] getSoundNameForAccountId:@"Default" buddyId:@"global"]; + if(soundName.length == 0) + { + return @""; + } + } + } + return soundName; +} + + +-(NSData*) getSoundDataForSenderJID:(NSString*) senderJID andReceiverJID:(NSString*) receiverJID +{ + return [[DataLayer sharedInstance] getSoundDataForAccountId:receiverJID buddyId:senderJID]; +} + +-(void) saveSoundData:(NSData*) soundData forSenderJID:(NSString*) senderJID andReceiverJID:(NSString*) receiverJID WithSoundFileName:(NSString*) filename isCustomSound:(NSNumber*)isCustom +{ + [[DataLayer sharedInstance] setAlertSoundWithAccountId:receiverJID buddyId:senderJID soundName:filename soundData:soundData isCustom:isCustom]; +} + +- (void) checkAndCreateAlertSoundsTable +{ + [[DataLayer sharedInstance] checkAndCreateAlertSoundsTable]; +} + +-(NSNumber*) getIsCustomSoundForAccountId:(NSString*) accountId buddyId:(NSString*) buddyId +{ + return [[DataLayer sharedInstance] getIsCustomSoundForAccountId:accountId buddyId:buddyId]; +} + +-(void) deleteContactForAccountId:(NSString*) accountId +{ + [[DataLayer sharedInstance] deleteSoundsForBuddyId:accountId]; +} + + + + +@end + diff --git a/Monal/Classes/SoundPickerView.swift b/Monal/Classes/SoundPickerView.swift new file mode 100644 index 0000000000..2be09c46c2 --- /dev/null +++ b/Monal/Classes/SoundPickerView.swift @@ -0,0 +1,147 @@ +// +// SoundPickerView.swift +// Monal +// +// Created by 阿栋 on 3/29/24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +import SwiftUI +import UIKit +import AVFoundation + +struct DocumentPicker: UIViewControllerRepresentable { + var onPicked: (URL) -> Void + var onDismiss: () -> Void + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIViewController(context: Context) -> UIDocumentPickerViewController { + let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.audio], asCopy: true) + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) { + } + + class Coordinator: NSObject, UIDocumentPickerDelegate, UINavigationControllerDelegate { + var parent: DocumentPicker + + init(_ documentPicker: DocumentPicker) { + self.parent = documentPicker + } + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + guard let pickedURL = urls.first else { return } + parent.onPicked(pickedURL) + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + parent.onDismiss() + } + } +} + +struct SoundPickerView: View { + @Environment(\.presentationMode) var presentationMode + @State private var showDocumentPicker = false + @State private var selectedAudioURL: URL? + @State private var audioPlayer: AVAudioPlayer? + @State private var audioData: Data? + @State private var selectedAudioFileName: String? + + + let contact: ObservableKVOWrapper? + let receiverJID: String + let senderJID: String + let onSoundPicked: (URL?) -> Void + let delegate: SheetDismisserProtocol + + + var body: some View { + NavigationView { + Form { + Section(header: Text("SELECT A SOUND TO PLAY WITH NOTIFICATIONS.")) { + Button(action: { + showDocumentPicker = true + }) { + HStack { + let custom = MLSoundManager.sharedInstance().getIsCustomSound(forAccountId: receiverJID, buddyId: senderJID) + let soundName = MLSoundManager.sharedInstance().getSoundName(forSenderJID: senderJID, andReceiverJID: receiverJID) + let textContent = (custom == 1 && audioData != nil && selectedAudioFileName == nil) ? soundName : (selectedAudioFileName ?? "Select sound file") + Text(textContent) + .foregroundColor(.primary) + Spacer() + if audioData != nil { + Button(action: { + self.selectedAudioURL = nil + self.audioPlayer?.stop() + self.audioPlayer = nil + }) { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.red) + } + .buttonStyle(BorderlessButtonStyle()) + Button(action: { + playAudioWithData(data: audioData!) + }) { + Image(systemName: "play.circle") + .foregroundColor(.blue) + } + .buttonStyle(BorderlessButtonStyle()) + } + } + } + } + } + .navigationBarTitle("Sound Selection", displayMode: .inline) + .navigationBarItems(trailing: Button("Save") { + if let selectedURL = selectedAudioURL { + onSoundPicked(selectedURL) + presentationMode.wrappedValue.dismiss() + } else { + self.selectedAudioURL = nil + onSoundPicked(self.selectedAudioURL) + presentationMode.wrappedValue.dismiss() + } + }) + .sheet(isPresented: $showDocumentPicker) { + DocumentPicker(onPicked: { url in + do { + selectedAudioURL = url + + let data = try Data(contentsOf: url) + self.audioData = data + self.selectedAudioFileName = url.lastPathComponent + self.playAudioWithData(data: data) + } catch { + DDLogError("Unable to load audio data: \(error)") + } + self.showDocumentPicker = false + }, onDismiss: { + self.showDocumentPicker = false + }) + } + .onAppear { + let custom = MLSoundManager.sharedInstance().getIsCustomSound(forAccountId: receiverJID, buddyId: senderJID) + if custom == 1 { + let data = MLSoundManager.sharedInstance().getSoundData(forSenderJID: senderJID, andReceiverJID: receiverJID) + self.audioData = data + } + } + } + } + + func playAudioWithData(data: Data) { + do { + audioPlayer = try AVAudioPlayer(data: data) + audioPlayer?.prepareToPlay() + audioPlayer?.play() + } catch { + DDLogError("Cannot play audio: \(error)") + } + } +} diff --git a/Monal/Classes/SoundsSettingView.swift b/Monal/Classes/SoundsSettingView.swift new file mode 100644 index 0000000000..860492b62d --- /dev/null +++ b/Monal/Classes/SoundsSettingView.swift @@ -0,0 +1,185 @@ +// +// SoundsSettingView.swift +// Monal +// +// Created by 阿栋 on 4/3/24. +// Copyright © 2024 monal-im.org. All rights reserved. +// + +import SwiftUI +import AVFoundation + +struct SoundsSettingView: View { + @State private var selectedSound: String + @State private var playSounds: Bool + @State private var audioPlayer: AVAudioPlayer? + @State private var showingSoundPicker = false + @State private var connectedAccounts: [xmpp] + @State private var selectedAccount = -1; + + let sounds: [String] = MLSoundManager.sharedInstance().listBundledSounds() + + let contact: ObservableKVOWrapper? + let delegate: SheetDismisserProtocol + + init(contact: ObservableKVOWrapper?, delegate: SheetDismisserProtocol) { + self.contact = contact + self.delegate = delegate + _playSounds = State(initialValue: HelperTools.defaultsDB().bool(forKey: "Sound")) + self.connectedAccounts = MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp] + var soundFileName: String + let receiverJID = "Default" + let senderJID = contact?.obj.contactJid.lowercased() ?? "global" + soundFileName = MLSoundManager.sharedInstance().getSoundName(forSenderJID: senderJID, andReceiverJID: receiverJID) + if (!sounds.contains(soundFileName) && soundFileName != "") { + soundFileName = "Custom Sound" + } else if soundFileName == "" { + soundFileName = "System Sound" + } + _selectedSound = State(initialValue: soundFileName) + } + + + var body: some View { + List { + if (contact == nil) { + Section { + Toggle(isOn: $playSounds) { + Text("Play Sounds") + } + .onChange(of: playSounds) { newValue in + HelperTools.defaultsDB().setValue(newValue, forKey: "Sound") + } + } + } + + if connectedAccounts.count >= 1 { + Picker("Use account", selection: $selectedAccount) { + Text("Default").tag(-1) + ForEach(Array(self.connectedAccounts.enumerated()), id: \.element) { idx, account in + Text(account.connectionProperties.identity.jid).tag(idx) + } + } + .pickerStyle(.menu) + .onChange(of: selectedAccount) { newValue in + let account = selectedAccount == -1 ? nil : self.connectedAccounts[self.selectedAccount] + let receiverJID = account == nil ? "Default" : account!.connectionProperties.identity.jid.lowercased() + let senderJID = contact?.obj.contactJid.lowercased() ?? "global" + var soundFileName = MLSoundManager.sharedInstance().getSoundName(forSenderJID: senderJID, andReceiverJID: receiverJID) + if soundFileName == "" { + soundFileName = "System Sound" + } + selectedSound = soundFileName + } + } + + if playSounds { + Section { + HStack { + Text("Custom Sound") + .onTapGesture { + self.showingSoundPicker = true + } + + Spacer() + + if selectedSound == "Custom Sound" { + Image(systemName: "checkmark") + .foregroundColor(.blue) + } + } + .sheet(isPresented: $showingSoundPicker) { + let account = selectedAccount == -1 ? nil : self.connectedAccounts[self.selectedAccount] + let receiverJID = selectedAccount == -1 ? "Default" : account!.connectionProperties.identity.jid.lowercased() + let senderJID = contact?.obj.contactJid.lowercased() ?? "global" + LazyClosureView(SoundPickerView(contact: contact, receiverJID: receiverJID, senderJID: senderJID, onSoundPicked: { (url: URL?) in + if (url != nil) { + do { + let soundData = try Data(contentsOf: url!) + self.selectedSound = "Custom Sound" + let soundFileName = url!.lastPathComponent + MLSoundManager.sharedInstance().saveSound(soundData, forSenderJID: senderJID, andReceiverJID: receiverJID, withSoundFileName: soundFileName, isCustomSound: 1) + } catch { + DDLogDebug("Error playing sound: \(error)") + } + } + }, delegate: delegate)) + } + } + } + + + if playSounds { + soundSelectionSection + } + + if playSounds { + Section { + HStack { + Spacer() + Text("Sounds courtesy Emrah") + .foregroundColor(.gray) + Spacer() + } + } + } + } + .navigationBarTitle("Sounds", displayMode: .inline) + .listStyle(GroupedListStyle()) + } + + var soundSelectionSection: some View { + Section(header: Text("SELECT SOUNDS THAT ARE PLAYED WITH NEW MESSAGE NOTIFICATIONS. DEFAULT IS XYLOPHONE.")) { + HStack { + Text("System Sound") + Spacer() + if selectedSound == "System Sound" { + Image(systemName: "checkmark") + .foregroundColor(.blue) + } + } + .contentShape(Rectangle()) + .onTapGesture { + self.selectedSound = "System Sound" + let account = selectedAccount == -1 ? nil : self.connectedAccounts[self.selectedAccount] + let receiverJID = account == nil ? "Default" : account!.connectionProperties.identity.jid.lowercased() + let senderJID = contact?.obj.contactJid.lowercased() ?? "global" + DataLayer.sharedInstance().deleteSound(forAccountId: receiverJID, buddyId: senderJID) + self.audioPlayer?.stop() + } + + ForEach(sounds.filter { $0 != "System Sound" }, id: \.self) { sound in + HStack { + Text(sound) + Spacer() + if sound == selectedSound { + Image(systemName: "checkmark") + .foregroundColor(.blue) + } + } + .contentShape(Rectangle()) + .onTapGesture { + self.selectedSound = sound + self.playSound(soundName: sound) + } + } + } + } + + func playSound(soundName: String) { + guard let url = Bundle.main.url(forResource: soundName, withExtension: "aif", subdirectory: "AlertSounds") else { return } + do { + let soundData = try Data(contentsOf: url) + audioPlayer = try AVAudioPlayer(data: soundData) + audioPlayer?.play() + let account = selectedAccount == -1 ? nil : self.connectedAccounts[self.selectedAccount] + let receiverJID = account == nil ? "Default" : account!.connectionProperties.identity.jid.lowercased() + let senderJID = contact?.obj.contactJid.lowercased() ?? "global" + let soundFileName = self.selectedSound + MLSoundManager.sharedInstance().saveSound(soundData, forSenderJID: senderJID, andReceiverJID: receiverJID, withSoundFileName: soundFileName, isCustomSound: 0) + } catch { + DDLogDebug("Error playing sound: \(error)") + } + } +} + diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index cd289effe1..151e3d12a0 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -421,6 +421,16 @@ class SwiftuiInterface : NSObject { host.rootView = AnyView(UIKitWorkaround(BackgroundSettings(contact:contactArg, delegate:delegate))) return host } + + @objc + func makeSoundSettings(_ contact: MLContact?) -> UIViewController { + let delegate = SheetDismisserProtocol() + let host = UIHostingController(rootView: AnyView(EmptyView())) + delegate.host = host + let contactArg = contact != nil ? ObservableKVOWrapper(contact!) : nil + host.rootView = AnyView(UIKitWorkaround(SoundsSettingView(contact: contactArg, delegate: delegate))) + return host + } @objc func makeAddContactView(dismisser: @escaping (MLContact) -> ()) -> UIViewController { diff --git a/Monal/monalxmpp/monalxmpp.h b/Monal/monalxmpp/monalxmpp.h index 8c5b67e332..5ac76ff23a 100644 --- a/Monal/monalxmpp/monalxmpp.h +++ b/Monal/monalxmpp/monalxmpp.h @@ -23,5 +23,6 @@ FOUNDATION_EXPORT const unsigned char monalxmppVersionString[]; #import "MLImageManager.h" #import "MLMucProcessor.h" #import "MLVoIPProcessor.h" +#import "MLSoundManager.h" #import "MLCall.h" #import "HelperTools.h"