// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2018-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #import "NotificationManager.h" #import "UserSettings.h" #import #import #import "NSString+Hex.h" #import "AppDelegate.h" #import "EntityFetcher.h" #import "UIDefines.h" #import #import "BundleUtil.h" #import "PushPayloadDecryptor.h" #import "ContactStore.h" #import "AppGroup.h" #import "ServerConnector.h" #import "TextStyleUtils.h" #import "Threema-Swift.h" #import "AbstractGroupMessage.h" #import "Conversation.h" #import "Contact.h" #import "GroupImageMessage.h" #import "GroupVideoMessage.h" #import "ImageMessage.h" #import "VideoMessage.h" #import "BoxImageMessage.h" #import "BoxVideoMessage.h" #import "ValidationLogger.h" @implementation NotificationManager { SystemSoundID receivedMessageSound; CFTimeInterval lastReceivedMessageSound; } + (NotificationManager *)sharedInstance { static NotificationManager *sharedInstance; static dispatch_once_t pred; dispatch_once(&pred, ^{ sharedInstance = [[NotificationManager alloc] init]; }); return sharedInstance; } - (id)init { self = [super init]; if (self) { /* Get sounds ready */ NSString *soundPath = [BundleUtil pathForResource:@"received_message" ofType:@"caf"]; CFURLRef baseURL = (__bridge CFURLRef)[NSURL fileURLWithPath:soundPath]; AudioServicesCreateSystemSoundID(baseURL, &receivedMessageSound); } return self; } - (void)updateUnreadMessagesCount:(BOOL)unloadedMessage { NSNumber *unread = [self unreadMessagesCount:unloadedMessage]; [[NSNotificationCenter defaultCenter] postNotificationName:@"ThreemaUnreadMessagesCountChanged" object:nil userInfo:[NSDictionary dictionaryWithObject:unread forKey:@"unread"]]; dispatch_async(dispatch_get_main_queue(), ^{ [UIApplication sharedApplication].applicationIconBadgeNumber = [unread integerValue]; }); } - (NSNumber *)unreadMessagesCount:(BOOL)unloadedMessage { EntityManager *entityManager = [[EntityManager alloc] init]; NSArray *conversations = [entityManager.entityFetcher allConversations]; int unread = 0; if (unloadedMessage) unread++; for (Conversation *conversation in conversations) { int count = [conversation.unreadMessageCount intValue]; if (count > 0) { unread += [conversation.unreadMessageCount intValue]; } } NSString *badgeValue = nil; if (unread > 0) badgeValue = [NSString stringWithFormat:@"%d", unread]; __block UITabBarController *mainTabBar; if ([NSThread isMainThread]) { mainTabBar = [AppDelegate getMainTabBarController]; if (mainTabBar && [mainTabBar isKindOfClass:[UITabBarController class]]) { [[mainTabBar.tabBar.items objectAtIndex:kChatTabBarIndex] setBadgeValue:badgeValue]; } } else { dispatch_sync(dispatch_get_main_queue(), ^{ mainTabBar = [AppDelegate getMainTabBarController]; if (mainTabBar && [mainTabBar isKindOfClass:[UITabBarController class]]) { [[mainTabBar.tabBar.items objectAtIndex:kChatTabBarIndex] setBadgeValue:badgeValue]; } }); } return [NSNumber numberWithInt:unread]; } - (void)handleVoIPPush:(NSDictionary *)payload withCompletionHandler:(void (^)(void))completion { if ([[MyIdentityStore sharedMyIdentityStore] isKeychainLocked]) { if (payload[@"threema"] != nil) { [NotificationManager showNoAccessToDatabaseNotification]; } [self waitForSeconds:2 finish:^{ exit(0); }]; // The keychain is locked; we cannot proceed. The UI will show the ProtectedDataUnavailable screen // at this point. To prevent this screen from appearing when the user unlocks their device after we // have processed the push, we exit now so that the process will restart after the device is unlocked. } else { WebClientSession *currentSession = nil; if (payload[@"3mw"] != nil) { NSDictionary *webPayload = payload[@"3mw"]; if (webPayload[@"wcs"] != nil) { currentSession = [[WebClientSessionStore shared] webClientSessionForHash:webPayload[@"wcs"]]; } if (currentSession != nil) { int webClientProtocolVersion = [webPayload[@"wcv"] intValue]; if (![currentSession.version isEqualToNumber:[NSNumber numberWithInt:webClientProtocolVersion]]) { // show error NSString *title; NSString *body; if (webClientProtocolVersion > [currentSession.version intValue]) { title = NSLocalizedString(@"webClientSession_error_updateApp_title", nil); body = NSLocalizedString(@"webClientSession_error_updateApp_message", nil); } else { if ([currentSession.selfHosted boolValue] == YES) { title = NSLocalizedString(@"webClientSession_error_updateServer_title", nil); body = NSLocalizedString(@"webClientSession_error_updateServer_message", nil); } else { title = NSLocalizedString(@"webClientSession_error_wrongVersion_title", nil); body = NSLocalizedString(@"webClientSession_error_wrongVersion_message", nil); } } [self showThreemaWebErrorWithTitle:title body:body]; [[BackgroundTaskManager shared] cancelBackgroundTaskWithKey:kAppPushBackgroundTask]; completion(); return; } [self loadMessages:payload currentSession:currentSession withCompletionHandler:completion]; } else { [[ValidationLogger sharedValidationLogger] logString:@"Threema Web: Unknown session try to connect; Session blocked"]; [[BackgroundTaskManager shared] cancelBackgroundTaskWithKey:kAppPushBackgroundTask]; completion(); } } else { [self loadMessages:payload currentSession:currentSession withCompletionHandler:completion]; } } } - (void)loadMessages:(NSDictionary *)payload currentSession:(WebClientSession *)currentSession withCompletionHandler:(void (^)(void))completion { NSDictionary *threemaDict = [PushPayloadDecryptor decryptPushPayload:payload[ThreemaPushNotificationDictionaryKey]]; NSString *messageId = threemaDict[ThreemaPushNotificationDictionaryMessageIdKey]; NSString *senderId = threemaDict[ThreemaPushNotificationDictionaryFromKey]; [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Push: Received Push Notification for %@", messageId]]; if (payload[@"3mw"] != nil) { if (currentSession != nil) { [[WCSessionManager shared] connectWithAuthToken:nil wca: payload[@"wca"] webClientSession:currentSession]; [[DatabaseManager dbManager] refreshDirtyObjects]; } else { // there is no local connection [[BackgroundTaskManager shared] cancelBackgroundTaskWithKey:kAppPushBackgroundTask]; completion(); return; } [[ServerConnector sharedServerConnector] connect]; if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive && [[WCSessionManager shared] isRunningWCSession]) { [[BackgroundTaskManager shared] newBackgroundTaskWithKey:kAppWCBackgroundTask timeout:kAppWCBackgroundTaskTime completionHandler:^{ [[BackgroundTaskManager shared] cancelBackgroundTaskWithKey:kAppPushBackgroundTask]; completion(); }]; } else { [[BackgroundTaskManager shared] cancelBackgroundTaskWithKey:kAppPushBackgroundTask]; completion(); } } else { if (senderId == nil && messageId == nil && threemaDict == nil) { [PendingMessage createTestNotificationWithPayload:payload completion:^{ [[BackgroundTaskManager shared] cancelBackgroundTaskWithKey:kAppPushBackgroundTask]; completion(); }]; } else { [[ValidationLogger sharedValidationLogger] logString:@"Threema Web: loadMessages --> connect all running sessions"]; [[WCSessionManager shared] connectAllRunningSessions]; [[DatabaseManager dbManager] refreshDirtyObjects]; [[PendingMessagesManager shared] pendingMessageWithSenderId:senderId messageId:messageId abstractMessage:nil threemaDict:threemaDict completion:^(PendingMessage *pendingMessage) { [[BackgroundTaskManager shared] newBackgroundTaskWithKey:kAppPushBackgroundTask timeout:kAppPushBackgroundTaskTime completionHandler:^{ if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive && [[WCSessionManager shared] isRunningWCSession]) { [[BackgroundTaskManager shared] newBackgroundTaskWithKey:kAppWCBackgroundTask timeout:kAppWCBackgroundTaskTime completionHandler:nil]; } if (pendingMessage != nil) { pendingMessage.completionHandler = completion; } else { completion(); } [[ServerConnector sharedServerConnector] connect]; }]; }]; } } } #pragma mark - Private functions - (void)showThreemaWebErrorWithTitle:(NSString *)title body:(NSString *)body { if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) { UNMutableNotificationContent *notification = [[UNMutableNotificationContent alloc] init]; notification.title = title; notification.body = body; if (![[UserSettings sharedUserSettings].pushSound isEqualToString:@"none"]) { notification.sound = [UNNotificationSound soundNamed:[NSString stringWithFormat:@"%@.caf", [UserSettings sharedUserSettings].pushSound]]; } UNNotificationRequest *notificationRequest = [UNNotificationRequest requestWithIdentifier:@"ThreemaWebError" content:notification trigger:nil]; UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center addNotificationRequest:notificationRequest withCompletionHandler:^(NSError * _Nullable error) { }]; } else { [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:title message:body actionOk:nil]; } } - (void)playReceivedMessageSound { CFTimeInterval curTime = CACurrentMediaTime(); /* play sound only twice per second */ if (curTime - lastReceivedMessageSound > 0.5) { if ([UserSettings sharedUserSettings].inAppSounds && [UserSettings sharedUserSettings].inAppVibrate) AudioServicesPlayAlertSound(receivedMessageSound); else if ([UserSettings sharedUserSettings].inAppSounds) AudioServicesPlaySystemSound(receivedMessageSound); else if ([UserSettings sharedUserSettings].inAppVibrate) AudioServicesPlayAlertSound(kSystemSoundID_Vibrate); } lastReceivedMessageSound = curTime; } - (void)waitForSeconds:(int)count finish:(void(^)(void))finish { if (count > 0 && [AppGroup getActiveType] == AppGroupTypeApp) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self waitForSeconds:count-1 finish:finish]; }); } else { finish(); } } #pragma mark - Static functions + (void)showNoAccessToDatabaseNotification { UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.title = NSLocalizedString(@"new_message_no_access_title", @""); content.body = NSLocalizedString(@"new_message_no_access_message", @""); content.badge = @1; UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"NoAccessToDB" content:content trigger:nil]; [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:nil]; } /** Generate push settings for all groups, will be run once when upgrade app. */ + (void)generatePushSettingForAllGroups { if ([UserSettings sharedUserSettings].pushGroupGenerated == NO) { NSMutableOrderedSet *pushSettings = [[NSMutableOrderedSet alloc] initWithOrderedSet:[UserSettings sharedUserSettings].pushSettingsList]; EntityManager *entityManager = [[EntityManager alloc] init]; NSArray *allGroupConversations = [entityManager.entityFetcher allGroupConversations]; for (Conversation *conversation in allGroupConversations) { NSString *identity = [NSString stringWithHexData:conversation.groupId]; PushSetting *pushSetting = [PushSetting findPushSettingForIdentity:identity pushSettingList:pushSettings]; if (pushSetting == nil) { PushSetting *tmpPushSetting = [PushSetting new]; tmpPushSetting.identity = identity; tmpPushSetting.type = kPushSettingTypeOn; tmpPushSetting.periodOffTime = 0; tmpPushSetting.periodOffTillDate = nil; tmpPushSetting.silent = false; tmpPushSetting.mentions = false; [pushSettings addObject:tmpPushSetting.buildDict]; } } [[UserSettings sharedUserSettings] setPushSettingsList:pushSettings]; [[UserSettings sharedUserSettings] setPushGroupGenerated:YES]; } } @end