session-ios/Signal/src/network/PushManager.m

470 lines
20 KiB
Objective-C

//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "PushManager.h"
#import "AppDelegate.h"
#import "OWSContactsManager.h"
#import "Signal-Swift.h"
#import "ThreadUtil.h"
#import <SignalServiceKit/NSDate+OWS.h>
#import <SignalServiceKit/OWSMessageReceiver.h>
#import <SignalServiceKit/OWSMessageSender.h>
#import <SignalServiceKit/OWSReadReceiptManager.h>
#import <SignalServiceKit/OWSSignalService.h>
#import <SignalServiceKit/TSAccountManager.h>
#import <SignalServiceKit/TSIncomingMessage.h>
#import <SignalServiceKit/TSOutgoingMessage.h>
#import <SignalServiceKit/TSSocketManager.h>
NSString *const Signal_Thread_UserInfo_Key = @"Signal_Thread_Id";
NSString *const Signal_Message_UserInfo_Key = @"Signal_Message_Id";
NSString *const Signal_Full_New_Message_Category = @"Signal_Full_New_Message";
NSString *const Signal_Full_New_Message_Category_No_Longer_Verified =
@"Signal_Full_New_Message_Category_No_Longer_Verified";
NSString *const Signal_Message_Reply_Identifier = @"Signal_New_Message_Reply";
NSString *const Signal_Message_MarkAsRead_Identifier = @"Signal_Message_MarkAsRead";
@interface PushManager ()
@property (nonatomic) NSMutableArray *currentNotifications;
@property (nonatomic) UIBackgroundTaskIdentifier callBackgroundTask;
@property (nonatomic, readonly) OWSMessageSender *messageSender;
@property (nonatomic, readonly) OWSMessageFetcherJob *messageFetcherJob;
@property (nonatomic, readonly) CallUIAdapter *callUIAdapter;
@end
@implementation PushManager
+ (instancetype)sharedManager {
static PushManager *sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[self alloc] initDefault];
});
return sharedManager;
}
- (instancetype)initDefault
{
return [self initWithMessageFetcherJob:[Environment getCurrent].messageFetcherJob
storageManager:[TSStorageManager sharedManager]
callUIAdapter:[Environment getCurrent].callService.callUIAdapter
messageSender:[Environment getCurrent].messageSender];
}
- (instancetype)initWithMessageFetcherJob:(OWSMessageFetcherJob *)messageFetcherJob
storageManager:(TSStorageManager *)storageManager
callUIAdapter:(CallUIAdapter *)callUIAdapter
messageSender:(OWSMessageSender *)messageSender
{
self = [super init];
if (!self) {
return self;
}
_callUIAdapter = callUIAdapter;
_messageSender = messageSender;
_messageFetcherJob = messageFetcherJob;
_callBackgroundTask = UIBackgroundTaskInvalid;
// TODO: consolidate notification tracking with NotificationsManager, which also maintains a list of notifications.
_currentNotifications = [NSMutableArray array];
OWSSingletonAssert();
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleMessageRead:)
name:kIncomingMessageMarkedAsReadNotification
object:nil];
return self;
}
- (void)handleMessageRead:(NSNotification *)notification
{
OWSAssert([NSThread isMainThread]);
if ([notification.object isKindOfClass:[TSIncomingMessage class]]) {
TSIncomingMessage *message = (TSIncomingMessage *)notification.object;
DDLogDebug(@"%@ canceled notification for message:%@", self.tag, message);
[self cancelNotificationsWithThreadId:message.uniqueThreadId];
}
}
#pragma mark Manage Incoming Push
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
DDLogInfo(@"%@ received remote notification", self.tag);
[self.messageFetcherJob runAsync];
}
- (void)applicationDidBecomeActive {
[self.messageFetcherJob runAsync];
}
/**
* This code should in principle never be called. The only cases where it would be called are with the old-style
* "content-available:1" pushes if there is no "voip" token registered
*
*/
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
DDLogInfo(@"%@ received content-available push", self.tag);
// If we want to re-introduce silent pushes we can remove this assert.
OWSFail(@"Unexpected content-available push.");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 20 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
completionHandler(UIBackgroundFetchResultNewData);
});
}
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
{
DDLogInfo(@"%@ launched from local notification", self.tag);
NSString *_Nullable threadId = notification.userInfo[Signal_Thread_UserInfo_Key];
if (threadId) {
[Environment presentConversationForThreadId:threadId];
} else {
OWSFail(@"%@ threadId was unexpectedly nil in %s", self.tag, __PRETTY_FUNCTION__);
}
}
- (void)application:(UIApplication *)application
handleActionWithIdentifier:(NSString *)identifier
forLocalNotification:(UILocalNotification *)notification
completionHandler:(void (^)())completionHandler
{
DDLogInfo(@"%@ in %s", self.tag, __FUNCTION__);
[self application:application
handleActionWithIdentifier:identifier
forLocalNotification:notification
withResponseInfo:@{}
completionHandler:completionHandler];
}
- (void)application:(UIApplication *)application
handleActionWithIdentifier:(NSString *)identifier
forLocalNotification:(UILocalNotification *)notification
withResponseInfo:(NSDictionary *)responseInfo
completionHandler:(void (^)())completionHandler
{
DDLogInfo(@"%@ handling action with identifier: %@", self.tag, identifier);
if ([identifier isEqualToString:Signal_Message_Reply_Identifier]) {
NSString *threadId = notification.userInfo[Signal_Thread_UserInfo_Key];
if (threadId) {
TSThread *thread = [TSThread fetchObjectWithUniqueID:threadId];
NSString *replyText = responseInfo[UIUserNotificationActionResponseTypedTextKey];
[ThreadUtil sendMessageWithText:replyText
inThread:thread
messageSender:self.messageSender
success:^{
// TODO do we really want to mark them all as read?
[self markAllInThreadAsRead:notification.userInfo completionHandler:completionHandler];
}
failure:^(NSError *_Nonnull error) {
// TODO Surface the specific error in the notification?
DDLogError(@"Message send failed with error: %@", error);
UILocalNotification *failedSendNotif = [[UILocalNotification alloc] init];
failedSendNotif.alertBody =
[NSString stringWithFormat:NSLocalizedString(@"NOTIFICATION_SEND_FAILED", nil), [thread name]];
failedSendNotif.userInfo = @{ Signal_Thread_UserInfo_Key : thread.uniqueId };
[self presentNotification:failedSendNotif checkForCancel:NO];
completionHandler();
}];
}
} else if ([identifier isEqualToString:Signal_Message_MarkAsRead_Identifier]) {
// TODO mark all as read? Or just this one?
[self markAllInThreadAsRead:notification.userInfo completionHandler:completionHandler];
} else if ([identifier isEqualToString:PushManagerActionsAcceptCall]) {
NSString *localIdString = notification.userInfo[PushManagerUserInfoKeysLocalCallId];
if (!localIdString) {
DDLogError(@"%@ missing localIdString.", self.tag);
return;
}
NSUUID *localId = [[NSUUID alloc] initWithUUIDString:localIdString];
if (!localId) {
DDLogError(@"%@ localIdString failed to parse as UUID.", self.tag);
return;
}
[self.callUIAdapter answerCallWithLocalId:localId];
completionHandler();
} else if ([identifier isEqualToString:PushManagerActionsDeclineCall]) {
NSString *localIdString = notification.userInfo[PushManagerUserInfoKeysLocalCallId];
if (!localIdString) {
DDLogError(@"%@ missing localIdString.", self.tag);
return;
}
NSUUID *localId = [[NSUUID alloc] initWithUUIDString:localIdString];
if (!localId) {
DDLogError(@"%@ localIdString failed to parse as UUID.", self.tag);
return;
}
[self.callUIAdapter declineCallWithLocalId:localId];
completionHandler();
} else if ([identifier isEqualToString:PushManagerActionsCallBack]) {
NSString *recipientId = notification.userInfo[PushManagerUserInfoKeysCallBackSignalRecipientId];
if (!recipientId) {
DDLogError(@"%@ missing call back id", self.tag);
return;
}
[self.callUIAdapter startAndShowOutgoingCallWithRecipientId:recipientId];
completionHandler();
} else if ([identifier isEqualToString:PushManagerActionsShowThread]) {
NSString *threadId = notification.userInfo[Signal_Thread_UserInfo_Key];
if (threadId) {
[Environment presentConversationForThreadId:threadId];
} else {
OWSFail(@"%@ threadId was unexpectedly nil in action with identifier: %@", self.tag, identifier);
}
completionHandler();
} else {
OWSFail(@"%@ Unhandled action with identifier: %@", self.tag, identifier);
NSString *threadId = notification.userInfo[Signal_Thread_UserInfo_Key];
if (threadId) {
[Environment presentConversationForThreadId:threadId];
} else {
OWSFail(@"%@ threadId was unexpectedly nil in action with identifier: %@", self.tag, identifier);
}
completionHandler();
}
}
- (void)markAllInThreadAsRead:(NSDictionary *)userInfo completionHandler:(void (^)())completionHandler {
NSString *threadId = userInfo[Signal_Thread_UserInfo_Key];
TSThread *thread = [TSThread fetchObjectWithUniqueID:threadId];
[[TSStorageManager sharedManager].dbReadWriteConnection
asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
// TODO: I suspect we only want to mark the message in
// question as read.
[thread markAllAsReadWithTransaction:transaction];
}
completionBlock:^{
[[[Environment getCurrent] homeViewController] updateInboxCountLabel];
[self cancelNotificationsWithThreadId:threadId];
completionHandler();
}];
}
- (UIUserNotificationCategory *)fullNewMessageNotificationCategory {
UIMutableUserNotificationAction *action_markRead = [self markAsReadAction];
UIMutableUserNotificationAction *action_reply = [UIMutableUserNotificationAction new];
action_reply.identifier = Signal_Message_Reply_Identifier;
action_reply.title = NSLocalizedString(@"PUSH_MANAGER_REPLY", @"");
action_reply.destructive = NO;
action_reply.authenticationRequired = NO;
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(9, 0)) {
action_reply.behavior = UIUserNotificationActionBehaviorTextInput;
action_reply.activationMode = UIUserNotificationActivationModeBackground;
} else {
action_reply.activationMode = UIUserNotificationActivationModeForeground;
}
UIMutableUserNotificationCategory *messageCategory = [UIMutableUserNotificationCategory new];
messageCategory.identifier = Signal_Full_New_Message_Category;
[messageCategory setActions:@[ action_markRead, action_reply ] forContext:UIUserNotificationActionContextMinimal];
[messageCategory setActions:@[ action_markRead, action_reply ] forContext:UIUserNotificationActionContextDefault];
return messageCategory;
}
- (UIUserNotificationCategory *)fullNewMessageNoLongerVerifiedNotificationCategory
{
UIMutableUserNotificationAction *action_markRead = [self markAsReadAction];
UIMutableUserNotificationCategory *messageCategory = [UIMutableUserNotificationCategory new];
messageCategory.identifier = Signal_Full_New_Message_Category_No_Longer_Verified;
[messageCategory setActions:@[ action_markRead ] forContext:UIUserNotificationActionContextMinimal];
[messageCategory setActions:@[ action_markRead ] forContext:UIUserNotificationActionContextDefault];
return messageCategory;
}
- (UIMutableUserNotificationAction *)markAsReadAction
{
UIMutableUserNotificationAction *action = [UIMutableUserNotificationAction new];
action.identifier = Signal_Message_MarkAsRead_Identifier;
action.title = NSLocalizedString(@"PUSH_MANAGER_MARKREAD", nil);
action.destructive = NO;
action.authenticationRequired = NO;
action.activationMode = UIUserNotificationActivationModeBackground;
return action;
}
#pragma mark - Signal Calls
NSString *const PushManagerCategoriesIncomingCall = @"PushManagerCategoriesIncomingCall";
NSString *const PushManagerCategoriesMissedCall = @"PushManagerCategoriesMissedCall";
NSString *const PushManagerCategoriesMissedCallFromNoLongerVerifiedIdentity =
@"PushManagerCategoriesMissedCallFromNoLongerVerifiedIdentity";
NSString *const PushManagerActionsAcceptCall = @"PushManagerActionsAcceptCall";
NSString *const PushManagerActionsDeclineCall = @"PushManagerActionsDeclineCall";
NSString *const PushManagerActionsCallBack = @"PushManagerActionsCallBack";
NSString *const PushManagerActionsIgnoreIdentityChangeAndCallBack =
@"PushManagerActionsIgnoreIdentityChangeAndCallBack";
NSString *const PushManagerActionsShowThread = @"PushManagerActionsShowThread";
NSString *const PushManagerUserInfoKeysLocalCallId = @"PushManagerUserInfoKeysLocalCallId";
NSString *const PushManagerUserInfoKeysCallBackSignalRecipientId = @"PushManagerUserInfoKeysCallBackSignalRecipientId";
- (UIUserNotificationCategory *)signalIncomingCallCategory
{
UIMutableUserNotificationAction *acceptAction = [UIMutableUserNotificationAction new];
acceptAction.identifier = PushManagerActionsAcceptCall;
acceptAction.title = NSLocalizedString(@"ANSWER_CALL_BUTTON_TITLE", @"");
acceptAction.activationMode = UIUserNotificationActivationModeForeground;
acceptAction.destructive = NO;
acceptAction.authenticationRequired = NO;
UIMutableUserNotificationAction *declineAction = [UIMutableUserNotificationAction new];
declineAction.identifier = PushManagerActionsDeclineCall;
declineAction.title = NSLocalizedString(@"REJECT_CALL_BUTTON_TITLE", @"");
declineAction.activationMode = UIUserNotificationActivationModeBackground;
declineAction.destructive = NO;
declineAction.authenticationRequired = NO;
UIMutableUserNotificationCategory *callCategory = [UIMutableUserNotificationCategory new];
callCategory.identifier = PushManagerCategoriesIncomingCall;
[callCategory setActions:@[ acceptAction, declineAction ] forContext:UIUserNotificationActionContextMinimal];
[callCategory setActions:@[ acceptAction, declineAction ] forContext:UIUserNotificationActionContextDefault];
return callCategory;
}
- (UIUserNotificationCategory *)signalMissedCallCategory
{
UIMutableUserNotificationAction *callBackAction = [UIMutableUserNotificationAction new];
callBackAction.identifier = PushManagerActionsCallBack;
callBackAction.title = [CallStrings callBackButtonTitle];
callBackAction.activationMode = UIUserNotificationActivationModeForeground;
callBackAction.destructive = NO;
callBackAction.authenticationRequired = YES;
UIMutableUserNotificationCategory *missedCallCategory = [UIMutableUserNotificationCategory new];
missedCallCategory.identifier = PushManagerCategoriesMissedCall;
[missedCallCategory setActions:@[ callBackAction ] forContext:UIUserNotificationActionContextMinimal];
[missedCallCategory setActions:@[ callBackAction ] forContext:UIUserNotificationActionContextDefault];
return missedCallCategory;
}
- (UIUserNotificationCategory *)signalMissedCallWithNoLongerVerifiedIdentityChangeCategory
{
UIMutableUserNotificationAction *showThreadAction = [UIMutableUserNotificationAction new];
showThreadAction.identifier = PushManagerActionsShowThread;
showThreadAction.title = [CallStrings showThreadButtonTitle];
showThreadAction.activationMode = UIUserNotificationActivationModeForeground;
showThreadAction.destructive = NO;
showThreadAction.authenticationRequired = YES;
UIMutableUserNotificationCategory *rejectedCallCategory = [UIMutableUserNotificationCategory new];
rejectedCallCategory.identifier = PushManagerCategoriesMissedCallFromNoLongerVerifiedIdentity;
[rejectedCallCategory setActions:@[ showThreadAction ] forContext:UIUserNotificationActionContextMinimal];
[rejectedCallCategory setActions:@[ showThreadAction ] forContext:UIUserNotificationActionContextDefault];
return rejectedCallCategory;
}
#pragma mark Util
- (int)allNotificationTypes {
return UIUserNotificationTypeAlert | UIUserNotificationTypeSound | UIUserNotificationTypeBadge;
}
- (UIUserNotificationSettings *)userNotificationSettings
{
DDLogDebug(@"%@ registering user notification settings", self.tag);
UIUserNotificationSettings *settings = [UIUserNotificationSettings
settingsForTypes:(UIUserNotificationType)[self allNotificationTypes]
categories:[NSSet setWithObjects:[self fullNewMessageNotificationCategory],
[self fullNewMessageNoLongerVerifiedNotificationCategory],
[self signalIncomingCallCategory],
[self signalMissedCallCategory],
[self signalMissedCallWithNoLongerVerifiedIdentityChangeCategory],
nil]];
return settings;
}
- (BOOL)applicationIsActive {
UIApplication *app = [UIApplication sharedApplication];
if (app.applicationState == UIApplicationStateActive) {
return YES;
}
return NO;
}
// TODO: consolidate notification tracking with NotificationsManager, which also maintains a list of notifications.
- (void)presentNotification:(UILocalNotification *)notification checkForCancel:(BOOL)checkForCancel
{
dispatch_async(dispatch_get_main_queue(), ^{
NSString *threadId = notification.userInfo[Signal_Thread_UserInfo_Key];
if (checkForCancel && threadId != nil) {
// The longer we wait, the more obsolete notifications we can suppress -
// but the more lag we introduce to notification delivery.
const CGFloat kDelaySeconds = 0.5f;
notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:kDelaySeconds];
notification.timeZone = [NSTimeZone localTimeZone];
}
[[UIApplication sharedApplication] scheduleLocalNotification:notification];
[self.currentNotifications addObject:notification];
});
}
// TODO: consolidate notification tracking with NotificationsManager, which also maintains a list of notifications.
- (void)cancelNotificationsWithThreadId:(NSString *)threadId
{
dispatch_async(dispatch_get_main_queue(), ^{
NSMutableArray *toDelete = [NSMutableArray array];
[self.currentNotifications
enumerateObjectsUsingBlock:^(UILocalNotification *notif, NSUInteger idx, BOOL *stop) {
if ([notif.userInfo[Signal_Thread_UserInfo_Key] isEqualToString:threadId]) {
[[UIApplication sharedApplication] cancelLocalNotification:notif];
[toDelete addObject:notif];
}
}];
[self.currentNotifications removeObjectsInArray:toDelete];
});
}
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end