another.im-ios/Monal/Classes/MonalAppDelegate.m
2024-11-18 15:53:52 +01:00

2034 lines
101 KiB
Objective-C

//
// SworIMAppDelegate.m
// SworIM
//
// Created by Anurodh Pokharel on 11/16/08.
// Copyright __MyCompanyName__ 2008. All rights reserved.
//
#import <BackgroundTasks/BackgroundTasks.h>
#import "MonalAppDelegate.h"
#import "MLConstants.h"
#import "HelperTools.h"
#import "MLNotificationManager.h"
#import "DataLayer.h"
#import "MLImageManager.h"
#import "ActiveChatsViewController.h"
#import "IPC.h"
#import "MLProcessLock.h"
#import "MLFiletransfer.h"
#import "xmpp.h"
#import "MLNotificationQueue.h"
#import "MLSettingsAboutViewController.h"
#import "MLMucProcessor.h"
#import "MBProgressHUD.h"
#import "MLVoIPProcessor.h"
#import "MLUDPLogger.h"
#import "MLCrashReporter.h"
@import NotificationBannerSwift;
@import UserNotifications;
#import "MLXMPPManager.h"
#import <AVKit/AVKit.h>
#import "MLBasePaser.h"
#import "MLXMLNode.h"
#import "XMPPStanza.h"
#import "XMPPDataForm.h"
#import "XMPPIQ.h"
#import "XMPPPresence.h"
#import "XMPPMessage.h"
#import "chatViewController.h"
@import Intents;
#define GRACEFUL_TIMEOUT 20.0
#define BGPROCESS_GRACEFUL_TIMEOUT 60.0
typedef void (^pushCompletion)(UIBackgroundFetchResult result);
@interface MonalAppDelegate()
{
NSMutableDictionary* _wakeupCompletions;
UIBackgroundTaskIdentifier _bgTask;
BGTask* _bgProcessing;
BGTask* _bgRefreshing;
monal_void_block_t _backgroundTimer;
MLContact* _contactToOpen;
monal_id_block_t _completionToCall;
BOOL _shutdownPending;
BOOL _wasFreezed;
}
@end
@implementation MonalAppDelegate
// **************************** xml parser and query language tests ****************************
-(void) runParserTests
{
NSString* xml = @"<?xml version='1.0'?>\n\
<stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' xmlns='jabber:client' xml:lang='en' from='example.org' id='a344b8bb-518e-4456-9140-d15f66c1d2db'>\n\
<stream:features><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>SCRAM-SHA-1</mechanism><mechanism>PLAIN</mechanism></mechanisms></stream:features>\n\
<message from='test@example.org' id='some_id' xmlns='jabber:client'>\n\
<body>Message text</body>\n\
<body xmlns='urn:some:different:namespace'>This will NOT be used</body>\n\
</message>\n\
<iq id='18382ACA-EF9D-4BC9-8779-7901C63B6631' to='user1@example.org/Monal-iOS.ef313600' xmlns='jabber:client' type='result' from='luloku@conference.example.org'><query xmlns='http://jabber.org/protocol/disco#info'><feature var='http://jabber.org/protocol/muc#request'/><feature var='muc_hidden'/><feature var='muc_unsecured'/><feature var='muc_membersonly'/><feature var='muc_unmoderated'/><feature var='muc_persistent'/><identity type='text' name='testchat gruppe' category='conference'/><feature var='urn:xmpp:mam:2'/><feature var='urn:xmpp:sid:0'/><feature var='muc_nonanonymous'/><feature var='http://jabber.org/protocol/muc'/><feature var='http://jabber.org/protocol/muc#stable_id'/><feature var='http://jabber.org/protocol/muc#self-ping-optimization'/><feature var='jabber:iq:register'/><feature var='vcard-temp'/><x type='result' xmlns='jabber:x:data'><field type='hidden' var='FORM_TYPE'><value>http://jabber.org/protocol/muc#roominfo</value></field><field label='Description' var='muc#roominfo_description' type='text-single'><value/></field><field label='Number of occupants' var='muc#roominfo_occupants' type='text-single'><value>2</value></field><field label='Allow members to invite new members' var='{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites' type='boolean'><value>0</value></field><field label='Allow users to invite other users' var='muc#roomconfig_allowinvites' type='boolean'><value>0</value></field><field label='Title' var='muc#roomconfig_roomname' type='text-single'><value>testchat gruppe</value></field><field type='boolean' var='muc#roomconfig_changesubject'/><field type='text-single' var='{http://modules.prosody.im/mod_vcard_muc}avatar#sha1'/><field type='text-single' var='muc#roominfo_lang'><value/></field></x></query></iq>\n\
<iq id='605818D4-4D16-4ACC-B003-BFA3E11849E1' to='user@example.com/Monal-iOS.15e153a8' xmlns='jabber:client' type='result' from='asdkjfhskdf@messaging.one'><pubsub xmlns='http://jabber.org/protocol/pubsub'><subscription node='eu.siacs.conversations.axolotl.devicelist' subid='6795F13596465' subscription='subscribed' jid='user@example.com'/></pubsub></iq>\n\
<iq from='benvolio@capulet.lit/230193' id='disco1' to='juliet@capulet.lit/chamber' type='result'>\n\
<query xmlns='http://jabber.org/protocol/disco#info' node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>\n\
<identity xml:lang='en' category='client' name='Psi 0.11' type='pc'/>\n\
<identity xml:lang='el' category='client' name='Ψ 0.11' type='pc'/>\n\
<feature var='http://jabber.org/protocol/caps'/>\n\
<feature var='http://jabber.org/protocol/disco#info'/>\n\
<feature var='http://jabber.org/protocol/disco#items'/>\n\
<feature var='http://jabber.org/protocol/muc'/>\n\
<x xmlns='jabber:x:data' type='result'>\n\
<field var='FORM_TYPE' type='hidden'>\n\
<value>urn:xmpp:dataforms:softwareinfo</value>\n\
</field>\n\
<field var='ip_version'>\n\
<value>ipv4</value>\n\
<value>ipv6</value>\n\
</field>\n\
<field var='os'>\n\
<value>Mac</value>\n\
</field>\n\
<field var='os_version'>\n\
<value>10.5.1</value>\n\
</field>\n\
<field var='software'>\n\
<value>Psi</value>\n\
</field>\n\
<field var='software_version'>\n\
<value>0.11</value>\n\
</field>\n\
</x>\n\
</query>\n\
</iq>\n\
</stream:stream>";
DDLogInfo(@"creating parser delegate for xml: %@", xml);
//yes, but this is not insecure because these are string literals boxed into an NSArray below rather than containing unchecked user input
//see here: https://releases.llvm.org/13.0.0/tools/clang/docs/DiagnosticsReference.html#wformat-security
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wformat-security"
MLBasePaser* delegate = [[MLBasePaser alloc] initWithCompletion:^(MLXMLNode* _Nullable parsedStanza) {
if(parsedStanza != nil)
{
DDLogInfo(@"Got new parsed stanza: %@", parsedStanza);
for(NSString* query in @[
@"{http://jabber.org/protocol/disco#info}query/\\{http://jabber.org/protocol/muc#roominfo}result@muc#roomconfig_roomname\\",
@"/{jabber:client}iq/{http://jabber.org/protocol/pubsub}pubsub/items<node~eu\\.siacs\\.conversations\\.axolotl\\.bundles:[0-9]+>@node",
@"body#",
])
{
id result = [parsedStanza find:query];
DDLogDebug(@"Query: '%@', result: '%@'", query, result);
}
NSString* specialQuery1 = @"/<type=%@>/{http://jabber.org/protocol/pubsub}pubsub/subscription<node=%@><subscription=%s><jid=%@>";
id result = [parsedStanza find:specialQuery1, @"result", @"eu.siacs.conversations.axolotl.devicelist", "subscribed", @"user@example.com"];
DDLogDebug(@"Query: '%@', result: '%@'", specialQuery1, result);
//handle gajim disco hash testcase
if([parsedStanza check:@"/<id=disco1>"])
{
//the the original implementation in MLIQProcessor $$class_handler(handleEntityCapsDisco)
NSMutableArray* identities = [NSMutableArray new];
for(MLXMLNode* identity in [parsedStanza find:@"{http://jabber.org/protocol/disco#info}query/identity"])
[identities addObject:[NSString stringWithFormat:@"%@/%@/%@/%@", [identity findFirst:@"/@category"], [identity findFirst:@"/@type"], ([identity check:@"/@xml:lang"] ? [identity findFirst:@"/@xml:lang"] : @""), ([identity check:@"/@name"] ? [identity findFirst:@"/@name"] : @"")]];
NSSet* features = [NSSet setWithArray:[parsedStanza find:@"{http://jabber.org/protocol/disco#info}query/feature@var"]];
NSArray* forms = [parsedStanza find:@"{http://jabber.org/protocol/disco#info}query/{jabber:x:data}x"];
NSString* ver = [HelperTools getEntityCapsHashForIdentities:identities andFeatures:features andForms:forms];
DDLogDebug(@"Caps hash calculated: %@", ver);
MLAssert([@"q07IKJEyjvHSyhy//CH0CxmKi8w=" isEqualToString:ver], @"Caps hash NOT equal to testcase hash 'q07IKJEyjvHSyhy//CH0CxmKi8w='!");
}
}
}];
#pragma clang diagnostic pop
//create xml parser, configure our delegate and feed it with data
NSXMLParser* xmlParser = [[NSXMLParser alloc] initWithData:[xml dataUsingEncoding:NSUTF8StringEncoding]];
[xmlParser setShouldProcessNamespaces:YES];
[xmlParser setShouldReportNamespacePrefixes:YES]; //for debugging only
[xmlParser setShouldResolveExternalEntities:NO];
[xmlParser setDelegate:delegate];
DDLogInfo(@"calling parse");
[xmlParser parse]; //blocking operation
DDLogInfo(@"parse ended");
[DDLog flushLog];
//make sure apple's code analyzer will not reject the app for the appstore because of our call to exit()
#ifdef IS_ALPHA
exit(0);
#endif
}
-(void) runSDPTests
{
DDLogVerbose(@"SDP2XML: %@", [HelperTools sdp2xml:@"v=0\n\
o=- 2005859539484728435 2 IN IP4 127.0.0.1\n\
s=-\n\
t=0 0\n\
a=group:BUNDLE 0 1 2\n\
a=extmap-allow-mixed\n\
a=msid-semantic: WMS stream\n\
m=audio 9 UDP/TLS/RTP/SAVPF 111 63 9 102 0 8 13 110 126\n\
c=IN IP4 0.0.0.0\n\
a=candidate:1076231993 2 udp 41885694 198.51.100.52 50002 typ relay raddr 0.0.0.0 rport 0 generation 0 ufrag V4as network-id 2 network-cost 10\n\
a=rtcp:9 IN IP4 0.0.0.0\n\
a=ice-ufrag:Pt2c\n\
a=ice-pwd:XKe021opw+vupIkkLCI1+kP4\n\
a=ice-options:trickle renomination\n\
a=fingerprint:sha-256 1F:CE:47:40:5F:F2:FC:66:F2:21:F7:7D:3D:D6:0D:B0:67:6F:BD:CF:8B:0E:B7:90:5D:8C:33:9E:AD:F2:CB:FC\n\
a=setup:actpass\n\
a=mid:0\n\
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\n\
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\n\
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\n\
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\n\
a=sendrecv\n\
a=msid:stream audio0\n\
a=rtcp-mux\n\
a=rtpmap:111 opus/48000/2\n\
a=rtcp-fb:111 transport-cc\n\
a=fmtp:111 minptime=10;useinbandfec=1\n\
a=rtpmap:63 red/48000/2\n\
a=fmtp:63 111/111\n\
a=rtpmap:9 G722/8000\n\
a=rtpmap:102 ILBC/8000\n\
a=rtpmap:0 PCMU/8000\n\
a=rtpmap:8 PCMA/8000\n\
a=rtpmap:13 CN/8000\n\
a=rtpmap:110 telephone-event/48000\n\
a=rtpmap:126 telephone-event/8000\n\
a=ssrc:109112503 cname:vUpPwDICjVuwEwGO\n\
a=ssrc:109112503 msid:stream audio0\n\
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 103 35 36 104 105 106\n\
c=IN IP4 0.0.0.0\n\
a=rtcp:9 IN IP4 0.0.0.0\n\
a=ice-ufrag:Pt2c\n\
a=ice-pwd:XKe021opw+vupIkkLCI1+kP4\n\
a=ice-options:trickle renomination\n\
a=fingerprint:sha-256 1F:CE:47:40:5F:F2:FC:66:F2:21:F7:7D:3D:D6:0D:B0:67:6F:BD:CF:8B:0E:B7:90:5D:8C:33:9E:AD:F2:CB:FC\n\
a=setup:actpass\n\
a=mid:1\n\
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset\n\
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\n\
a=extmap:13 urn:3gpp:video-orientation\n\
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\n\
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\n\
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\n\
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\n\
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\n\
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\n\
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\n\
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\n\
a=sendrecv\n\
a=msid:stream video0\n\
a=rtcp-mux\n\
a=rtcp-rsize\n\
a=rtpmap:96 H264/90000\n\
a=rtcp-fb:96 goog-remb\n\
a=rtcp-fb:96 transport-cc\n\
a=rtcp-fb:96 ccm fir\n\
a=rtcp-fb:96 nack\n\
a=rtcp-fb:96 nack pli\n\
a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c34\n\
a=rtpmap:97 rtx/90000\n\
a=fmtp:97 apt=96\n\
a=rtpmap:98 H264/90000\n\
a=rtcp-fb:98 goog-remb\n\
a=rtcp-fb:98 transport-cc\n\
a=rtcp-fb:98 ccm fir\n\
a=rtcp-fb:98 nack\n\
a=rtcp-fb:98 nack pli\n\
a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e034\n\
a=rtpmap:99 rtx/90000\n\
a=fmtp:99 apt=98\n\
a=rtpmap:100 VP8/90000\n\
a=rtcp-fb:100 goog-remb\n\
a=rtcp-fb:100 transport-cc\n\
a=rtcp-fb:100 ccm fir\n\
a=rtcp-fb:100 nack\n\
a=rtcp-fb:100 nack pli\n\
a=rtpmap:101 rtx/90000\n\
a=fmtp:101 apt=100\n\
a=rtpmap:127 VP9/90000\n\
a=rtcp-fb:127 goog-remb\n\
a=rtcp-fb:127 transport-cc\n\
a=rtcp-fb:127 ccm fir\n\
a=rtcp-fb:127 nack\n\
a=rtcp-fb:127 nack pli\n\
a=rtpmap:103 rtx/90000\n\
a=fmtp:103 apt=127\n\
a=rtpmap:35 AV1/90000\n\
a=rtcp-fb:35 goog-remb\n\
a=rtcp-fb:35 transport-cc\n\
a=rtcp-fb:35 ccm fir\n\
a=rtcp-fb:35 nack\n\
a=rtcp-fb:35 nack pli\n\
a=rtpmap:36 rtx/90000\n\
a=fmtp:36 apt=35\n\
a=rtpmap:104 red/90000\n\
a=rtpmap:105 rtx/90000\n\
a=fmtp:105 apt=104\n\
a=rtpmap:106 ulpfec/90000\n\
a=ssrc-group:FID 3733210709 4025710505\n\
a=ssrc:3733210709 cname:vUpPwDICjVuwEwGO\n\
a=ssrc:3733210709 msid:stream video0\n\
a=ssrc:4025710505 cname:vUpPwDICjVuwEwGO\n\
a=ssrc:4025710505 msid:stream video0\n\
m=application 9 UDP/DTLS/SCTP webrtc-datachannel\n\
c=IN IP4 0.0.0.0\n\
a=ice-ufrag:Pt2c\n\
a=ice-pwd:XKe021opw+vupIkkLCI1+kP4\n\
a=ice-options:trickle renomination\n\
a=fingerprint:sha-256 1F:CE:47:40:5F:F2:FC:66:F2:21:F7:7D:3D:D6:0D:B0:67:6F:BD:CF:8B:0E:B7:90:5D:8C:33:9E:AD:F2:CB:FC\n\
a=setup:actpass\n\
a=mid:2\n\
a=sctp-port:5000\n\
a=max-message-size:262144\n" withInitiator:YES]);
}
$$class_handler(handlerTest01, $$ID(NSObject*, dummyObj))
DDLogError(@"HandlerTest01 completed");
$$
$$class_handler(handlerTest02, $$ID(monal_void_block_t, dummyCallback))
DDLogError(@"HandlerTest02 completed");
$$
-(void) runHandlerTests
{
DDLogError(@"NSClassFromString: '%@'", NSClassFromString(@"monal_void_block_t"));
if([^{} isKindOfClass:[NSObject class]])
DDLogError(@"isKindOfClass");
MLHandler* handler01 = $newHandler([self class], handlerTest01);
$call(handler01, $ID(dummyObj, [NSString new]));
MLHandler* handler02 = $newHandler([self class], handlerTest02);
$call(handler02, $ID(dummyCallback, ^{}));
}
-(id) init
{
//someone (suspect: AppKit) resets our exception handler between the call to [MonalAppDelegate initialize] and [MonalAppDelegate init]
[HelperTools installExceptionHandler];
self = [super init];
_bgTask = UIBackgroundTaskInvalid;
_wakeupCompletions = [NSMutableDictionary new];
DDLogVerbose(@"Setting _shutdownPending to NO...");
_shutdownPending = NO;
_wasFreezed = NO;
//[self runParserTests];
//[self runSDPTests];
//[HelperTools flushLogsWithTimeout:0.250];
//[self runHandlerTests];
return self;
}
#pragma mark - APNS notification
-(void) application:(UIApplication*) application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*) deviceToken
{
NSString* token = [HelperTools stringFromToken:deviceToken];
DDLogInfo(@"APNS token string: %@", token);
[[MLXMPPManager sharedInstance] setPushToken:token];
}
-(void) application:(UIApplication*) application didFailToRegisterForRemoteNotificationsWithError:(NSError*) error
{
DDLogError(@"APNS push reg error %@", error);
[[MLXMPPManager sharedInstance] removeToken];
[MLXMPPManager sharedInstance].apnsError = error;
}
#pragma mark - notification actions
-(void) updateUnread
{
DDLogInfo(@"Updating unread called");
//make sure unread badge matches application badge
NSNumber* unreadMsgCnt = [[DataLayer sharedInstance] countUnreadMessages];
[HelperTools dispatchAsync:YES reentrantOnQueue:dispatch_get_main_queue() withBlock:^{
NSInteger unread = 0;
if(unreadMsgCnt != nil)
unread = [unreadMsgCnt integerValue];
DDLogInfo(@"Updating unread badge to: %ld", (long)unread);
[[UNUserNotificationCenter currentNotificationCenter] setBadgeCount:unread withCompletionHandler:nil];
}];
}
#pragma mark - app life cycle
-(BOOL) application:(UIApplication*) application willFinishLaunchingWithOptions:(NSDictionary*) launchOptions
{
DDLogInfo(@"App launching with options: %@", launchOptions);
//init IPC and ProcessLock
[IPC initializeForProcess:@"MainApp"];
[MLProcessLock initializeForProcess:@"MainApp"];
//lock process and disconnect an already running NotificationServiceExtension
[MLProcessLock lock];
[[IPC sharedInstance] sendMessage:@"Monal.disconnectAll" withData:nil to:@"NotificationServiceExtension"];
//do MLFiletransfer cleanup tasks (do this in a new thread to parallelize it with our ping to the appex and don't slow down app startup)
//this will also migrate our old image cache to new MLFiletransfer cache
//BUT: don't do this if we are sending the sharesheet outbox
if(launchOptions[UIApplicationLaunchOptionsURLKey] == nil || ![launchOptions[UIApplicationLaunchOptionsURLKey] isEqual:kMonalOpenURL])
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
[MLFiletransfer doStartupCleanup];
});
//do image manager cleanup in a new thread to not slow down app startup
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
[[MLImageManager sharedInstance] cleanupHashes];
});
//only proceed with launching if the NotificationServiceExtension is *not* running
if([MLProcessLock checkRemoteRunning:@"NotificationServiceExtension"])
{
DDLogInfo(@"NotificationServiceExtension is running, waiting for its termination");
[MLProcessLock waitForRemoteTermination:@"NotificationServiceExtension" withLoopHandler:^{
[[IPC sharedInstance] sendMessage:@"Monal.disconnectAll" withData:nil to:@"NotificationServiceExtension"];
}];
}
return YES;
}
-(BOOL) application:(UIApplication*) application didFinishLaunchingWithOptions:(NSDictionary*) launchOptions
{
//this will use the cached values in defaultsDB, if possible
[[MLXMPPManager sharedInstance] setPushToken:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleScheduleBackgroundTaskNotification:) name:kScheduleBackgroundTask object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nowIdle:) name:kMonalIdle object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(filetransfersNowIdle:) name:kMonalFiletransfersIdle object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nowNotIdle:) name:kMonalNotIdle object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showConnectionStatus:) name:kXMPPError object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateUnread) name:kMonalNewMessageNotice object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateUnread) name:kMonalUpdateUnread object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(prepareForFreeze:) name:kMonalWillBeFreezed object:nil];
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = self;
//create notification categories with actions
UNNotificationAction* replyAction = [UNTextInputNotificationAction
actionWithIdentifier:@"REPLY_ACTION"
title:NSLocalizedString(@"Reply", @"")
options:UNNotificationActionOptionNone
icon:[UNNotificationActionIcon iconWithSystemImageName:@"arrowshape.turn.up.left"]
textInputButtonTitle:NSLocalizedString(@"Send", @"")
textInputPlaceholder:NSLocalizedString(@"Your answer", @"")
];
UNNotificationAction* markAsReadAction = [UNNotificationAction
actionWithIdentifier:@"MARK_AS_READ_ACTION"
title:NSLocalizedString(@"Mark as read", @"")
options:UNNotificationActionOptionNone
icon:[UNNotificationActionIcon iconWithSystemImageName:@"checkmark.bubble"]
];
UNNotificationAction* approveSubscriptionAction = [UNNotificationAction
actionWithIdentifier:@"APPROVE_SUBSCRIPTION_ACTION"
title:NSLocalizedString(@"Approve new contact", @"")
options:UNNotificationActionOptionNone
icon:[UNNotificationActionIcon iconWithSystemImageName:@"person.crop.circle.badge.checkmark"]
];
UNNotificationAction* denySubscriptionAction = [UNNotificationAction
actionWithIdentifier:@"DENY_SUBSCRIPTION_ACTION"
title:NSLocalizedString(@"Deny new contact", @"")
options:UNNotificationActionOptionNone
icon:[UNNotificationActionIcon iconWithSystemImageName:@"person.crop.circle.badge.xmark"]
];
UNAuthorizationOptions authOptions = UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionProvidesAppNotificationSettings;
#if TARGET_OS_MACCATALYST
authOptions |= UNAuthorizationOptionProvisional;
#endif
UNNotificationCategory* messageCategory = [UNNotificationCategory
categoryWithIdentifier:@"message"
actions:@[replyAction, markAsReadAction]
intentIdentifiers:@[]
options:UNNotificationCategoryOptionNone
];
UNNotificationCategory* subscriptionCategory = [UNNotificationCategory
categoryWithIdentifier:@"subscription"
actions:@[approveSubscriptionAction, denySubscriptionAction]
intentIdentifiers:@[]
options:UNNotificationCategoryOptionCustomDismissAction
];
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings* settings) {
DDLogInfo(@"Current notification settings: %@", settings);
}];
//request auth to show notifications and register our notification categories created above
[center requestAuthorizationWithOptions:authOptions completionHandler:^(BOOL granted, NSError* error) {
dispatch_async(dispatch_get_main_queue(), ^{
DDLogInfo(@"Got local notification authorization response: granted=%@, error=%@", bool2str(granted), error);
BOOL oldGranted = [[HelperTools defaultsDB] boolForKey:@"notificationsGranted"];
[[HelperTools defaultsDB] setBool:granted forKey:@"notificationsGranted"];
if(granted == YES)
{
if(!oldGranted)
{
//this is only needed for better UI (settings --> noifications should reflect the proper state)
//both invalidations are needed because we don't know the timing of this notification granting handler
DDLogInfo(@"Invalidating all account states...");
[[DataLayer sharedInstance] invalidateAllAccountStates]; //invalidate states for account objects not yet created
[[MLXMPPManager sharedInstance] reconnectAll]; //invalidate for account objects already created
}
//activate push
DDLogInfo(@"Registering for APNS...");
[[UIApplication sharedApplication] registerForRemoteNotifications];
[self->_voipProcessor voipRegistration];
}
else
{
//delete apns push token --> push will not be registered on our xmpp server anymore
DDLogWarn(@"Notifications disabled --> deleting APNS push token from user defaults!");
NSString* oldToken = [[HelperTools defaultsDB] objectForKey:@"pushToken"];
[[MLXMPPManager sharedInstance] removeToken];
if((oldToken != nil && oldToken.length != 0) || oldGranted)
{
//this is only needed for better UI (settings --> noifications should reflect the proper state)
//both invalidations are needed because we don't know the timing of this notification granting handler
DDLogInfo(@"Invalidating all account states...");
[[DataLayer sharedInstance] invalidateAllAccountStates]; //invalidate states for account objects not yet created
[[MLXMPPManager sharedInstance] reconnectAll]; //invalidate for account objects already created
}
}
});
}];
[center setNotificationCategories:[NSSet setWithObjects:messageCategory, subscriptionCategory , nil]];
UINavigationBarAppearance* appearance = [UINavigationBarAppearance new];
[appearance configureWithTransparentBackground];
appearance.backgroundColor = [UIColor systemBackgroundColor];
[[UINavigationBar appearance] setScrollEdgeAppearance:appearance];
[[UINavigationBar appearance] setStandardAppearance:appearance];
#if TARGET_OS_MACCATALYST
self.window.windowScene.titlebar.titleVisibility = UITitlebarTitleVisibilityHidden;
#endif
[[UINavigationBar appearance] setPrefersLargeTitles:YES];
//handle message notifications by initializing the MLNotificationManager
[MLNotificationManager sharedInstance];
//register BGTask
DDLogInfo(@"calling MonalAppDelegate configureBackgroundTasks");
[self configureBackgroundTasks];
// Play audio even if phone is in silent mode
[HelperTools configureDefaultAudioSession];
self.audioState = MLAudioStateNormal;
DDLogInfo(@"App started: %@", [HelperTools appBuildVersionInfoFor:MLVersionTypeLog]);
//init background/foreground status
//this has to be done here to make sure we have the correct state when he app got started through notification quick actions
//NOTE: the connectedXMPP array does not exist at this point --> calling this methods only updates the state without messing with the accounts themselves
if([UIApplication sharedApplication].applicationState==UIApplicationStateBackground)
[[MLXMPPManager sharedInstance] nowBackgrounded];
else
[[MLXMPPManager sharedInstance] nowForegrounded];
@synchronized(self) {
DDLogVerbose(@"Setting _shutdownPending to NO...");
_shutdownPending = NO;
}
[self addBackgroundTask];
//should any accounts connect?
[self connectIfNecessaryWithOptions:launchOptions];
//handle IPC messages (this should be done *after* calling connectIfNecessary to make sure any disconnectAll messages are handled properly
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(incomingIPC:) name:kMonalIncomingIPC object:nil];
#if TARGET_OS_MACCATALYST
//handle catalyst foregrounding/backgrounding of window
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowHandling:) name:@"NSWindowDidResignKeyNotification" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowHandling:) name:@"NSWindowDidBecomeKeyNotification" object:nil];
#endif
//initialize callkit (mus be done after connectIfNecessary to make sure the list of accounts is already populated when a voip push comes in)
_voipProcessor = [MLVoIPProcessor new];
/*
NSDictionary* options = launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey];
if(options != nil && [@"INSendMessageIntent" isEqualToString:options[UIApplicationLaunchOptionsUserActivityTypeKey]])
{
NSUserActivity* userActivity = options[@"UIApplicationLaunchOptionsUserActivityKey"];
DDLogError(@"intent: %@", userActivity.interaction);
}
*/
return YES;
}
-(BOOL) application:(UIApplication*) application continueUserActivity:(NSUserActivity*) userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>>* restorableObjects)) restorationHandler
{
DDLogDebug(@"Got continueUserActivity call...");
if([userActivity.interaction.intent isKindOfClass:[INStartCallIntent class]])
{
DDLogInfo(@"INStartCallIntent interaction: %@", userActivity.interaction);
INStartCallIntent* intent = (INStartCallIntent*)userActivity.interaction.intent;
if(intent.contacts.firstObject != nil)
{
INPersonHandle* contactHandle = intent.contacts.firstObject.personHandle;
DDLogInfo(@"INStartCallIntent with contact: %@", contactHandle.value);
NSArray<MLContact*>* contacts = [[DataLayer sharedInstance] contactListWithJid:contactHandle.value];
if([contacts count] == 0)
{
[self.activeChats showCallContactNotFoundAlert:contactHandle.value];
return NO;
}
//don't display account picker or open call ui if we have an already active call with any of the possible contacts
//the call ui will be brought into foreground by applicationWillEnterForeground: independently of this
for(MLContact* contact in contacts)
if([self.voipProcessor getActiveCallWithContact:contact] != nil)
return YES;
MLCallType callType = MLCallTypeAudio; //default is audio call
if(intent.callCapability == INCallCapabilityVideoCall)
callType = MLCallTypeVideo;
if([contacts count] > 1)
[self.activeChats presentAccountPickerForContacts:contacts andCallType:callType];
else
[self.activeChats callContact:contacts.firstObject withCallType:callType];
return YES;
}
}
else if([userActivity.interaction.intent isKindOfClass:[INSendMessageIntent class]])
{
DDLogError(@"Got INSendMessageIntent: %@", (INSendMessageIntent*)userActivity.interaction.intent);
}
return NO;
}
-(id) application:(UIApplication*) application handlerForIntent:(INIntent*) intent
{
DDLogError(@"Got intent: %@", intent);
return nil;
}
#if TARGET_OS_MACCATALYST
-(void) windowHandling:(NSNotification*) notification
{
if([notification.name isEqualToString:@"NSWindowDidResignKeyNotification"])
{
DDLogInfo(@"Window lost focus (key window)...");
[self updateUnread];
if(NSProcessInfo.processInfo.isLowPowerModeEnabled)
{
DDLogInfo(@"LowPowerMode is active: nowReallyBackgrounded to reduce power consumption");
[self nowReallyBackgrounded];
}
else
[[MLXMPPManager sharedInstance] noLongerInFocus];
}
else if([notification.name isEqualToString:@"NSWindowDidBecomeKeyNotification"])
{
DDLogInfo(@"Window got focus (key window)...");
[MLProcessLock lock];
@synchronized(self) {
DDLogVerbose(@"Setting _shutdownPending to NO...");
_shutdownPending = NO;
}
//cancel already running background timer, we are now foregrounded again
[self stopBackgroundTimer];
[self addBackgroundTask];
[[MLXMPPManager sharedInstance] nowForegrounded];
}
}
#endif
-(void) incomingIPC:(NSNotification*) notification
{
NSDictionary* message = notification.userInfo;
//another process tells us to disconnect all accounts
//this could happen if we are connecting (or even connected) in the background and the NotificationServiceExtension got started
//BUT: only do this if we are in background (we should never receive this if we are foregrounded)
MLAssert(![message[@"name"] isEqualToString:@"Monal.disconnectAll"], @"Got 'Monal.disconnectAll' while in mainapp. This should NEVER happen!", message);
if([message[@"name"] isEqualToString:@"Monal.connectIfNecessary"])
{
DDLogInfo(@"Got connectIfNecessary IPC message");
//(re)connect all accounts
[self connectIfNecessaryWithOptions:nil];
}
}
-(void) applicationDidBecomeActive:(UIApplication*) application
{
if([[MLXMPPManager sharedInstance] connectedXMPP].count > 0)
[self handleSpinner];
else
{
//hide spinner
[self.activeChats.spinner stopAnimating];
}
//report pending crashes
[MLCrashReporter reportPendingCrashes];
}
-(void) setActiveChats:(UIViewController*) activeChats
{
DDLogDebug(@"Active chats did load...");
_activeChats = (ActiveChatsViewController*)activeChats;
[self openChatOfContact:_contactToOpen withCompletion:_completionToCall];
}
#pragma mark - handling urls
/**
xmpp:romeo@montague.net?message;subject=Test%20Message;body=Here%27s%20a%20test%20message
xmpp:coven@chat.shakespeare.lit?join;password=cauldronburn
xmpp:example.com?register;preauth=3c7efeafc1bb10d034
xmpp:romeo@example.com?register;preauth=3c7efeafc1bb10d034
xmpp:contact@example.com?roster;preauth=3c7efeafc1bb10d034
xmpp:contact@example.com?roster;preauth=3c7efeafc1bb10d034;ibr=y
@link https://xmpp.org/extensions/xep-0147.html
@link https://docs.modernxmpp.org/client/invites/
*/
-(void) handleXMPPURL:(NSURL*) url
{
//make sure we have the active chats ui loaded and accessible
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
while(self.activeChats == nil)
usleep(100000);
dispatch_async(dispatch_get_main_queue(), ^{
//remove everything from our view queue (including currently displayed views)
//and add intro screens back to the queue, if needed, followed by the view handling the xmpp uri action
[self.activeChats resetViewQueue];
[self.activeChats dismissCompleteViewChainWithAnimation:NO andCompletion:^{
[self.activeChats segueToIntroScreensIfNeeded];
BOOL registerNeeded = [MLXMPPManager sharedInstance].connectedXMPP.count == 0;
NSURLComponents* components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
DDLogVerbose(@"URI path '%@'", components.path);
DDLogVerbose(@"URI query '%@'", components.query);
NSString* jid = components.path;
NSDictionary* jidParts = [HelperTools splitJid:jid];
BOOL isRegister = NO;
BOOL isRoster = NO;
BOOL isMucJoin = NO;
BOOL isIbr = NO;
NSString* preauthToken = nil;
NSMutableDictionary<NSNumber*, NSData*>* omemoFingerprints = [NSMutableDictionary new];
//someone had the really superior (NOT!) idea to split uri query parts by ';' instead of the standard '&'
//making all existing uri libs useless, see: https://xmpp.org/extensions/xep-0147.html
//blame this author: Peter Saint-Andre
NSArray* queryItems = [components.query componentsSeparatedByString:@";"];
for(NSString* item in queryItems)
{
NSArray* itemParts = [item componentsSeparatedByString:@"="];
NSString* name = itemParts[0];
NSString* value = @"";
if([itemParts count] > 1)
value = itemParts[1];
DDLogVerbose(@"URI part '%@' = '%@'", name, value);
if([name isEqualToString:@"register"])
isRegister = YES;
if([name isEqualToString:@"roster"])
isRoster = YES;
if([name isEqualToString:@"join"])
isMucJoin = YES;
if([name isEqualToString:@"ibr"] && [value isEqualToString:@"y"])
isIbr = YES;
if([name isEqualToString:@"preauth"])
preauthToken = [value copy];
if([name hasPrefix:@"omemo-sid-"])
{
NSNumber* sid = [NSNumber numberWithUnsignedInteger:(NSUInteger)[[name substringFromIndex:10] longLongValue]];
NSData* fingerprint = [HelperTools signalIdentityWithHexKey:value];
omemoFingerprints[sid] = fingerprint;
}
}
if(!jidParts[@"host"])
{
DDLogError(@"Ignoring xmpp: uri without host jid part!");
return;
}
#ifdef IS_QUICKSY
//make sure we hit the else below, even if (isRegister || (isRoster && registerNeeded)) == YES
if(NO)
;
#else
if(isRegister || (isRoster && registerNeeded))
{
NSString* username = nilDefault(jidParts[@"node"], @"");
NSString* host = jidParts[@"host"];
if(isRoster)
{
//isRoster variant does not specify a predefined username for the new account, register does (but this is still optional)
username = @"";
//isRoster variant without ibr does not specify a host to register on, too
if(!isIbr)
host = @"";
}
//show register view and, if isRoster, add contact as usual after register (e.g. call this method again)
weakify(self);
[self.activeChats showRegisterWithUsername:username onHost:host withToken:preauthToken usingCompletion:^(NSNumber* accountID) {
strongify(self);
DDLogVerbose(@"Got accountID for newly registered account: %@", accountID);
xmpp* account = [[MLXMPPManager sharedInstance] getEnabledAccountForID:accountID];
DDLogInfo(@"Got newly registered account: %@", account);
//this should never happen
MLAssert(account != nil, @"Can not use account after register!", (@{
@"components": components,
@"username": username,
@"host": host,
}));
//add given jid to our roster if in roster mode (e.g. the jid is not the jid we just registered as like in register mode)
if(account != nil && isRoster) //silence memory warning despite assertion above
return [self handleXMPPURL:url];
}];
}
#endif
//I know this if is moot, but I wanted to preserve the different cases:
//either we already have one or more accounts and the xmpp: uri is of type subscription (ibr does not matter here,
//because we already have an account) or muc join
//OR the xmpp: uri is a normal xmpp uri having only a jid we should add as our new contact (preauthToken will be nil in this case)
else if((!registerNeeded && (isRoster || isMucJoin)) || !registerNeeded)
{
if([MLXMPPManager sharedInstance].connectedXMPP.count == 1)
{
//the add contacts ui will check if the contact is already present on the selected account
xmpp* account = [[MLXMPPManager sharedInstance].connectedXMPP firstObject];
[self.activeChats showAddContactWithJid:jid preauthToken:preauthToken prefillAccount:account andOmemoFingerprints:omemoFingerprints];
}
else
//the add contacts ui will check if the contact is already present on the selected account
[self.activeChats showAddContactWithJid:jid preauthToken:preauthToken prefillAccount:nil andOmemoFingerprints:omemoFingerprints];
}
else
{
DDLogError(@"No account available to handel xmpp: uri!");
UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Error adding contact or channel", @"") message:NSLocalizedString(@"No account available to handel 'xmpp:' URI!", @"") preferredStyle:UIAlertControllerStyleAlert];
[messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) {
}]];
[self.activeChats presentViewController:messageAlert animated:YES completion:nil];
}
}];
});
});
}
-(BOOL) application:(UIApplication*) app openURL:(NSURL*) url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*) options
{
DDLogInfo(@"Got openURL for '%@' with options: %@", url, options);
if([url.scheme isEqualToString:@"xmpp"]) //for xmpp uris
{
[self handleXMPPURL:url];
return YES;
}
else if([url.scheme isEqualToString:kMonalOpenURL.scheme]) //app opened via sharesheet
{
//make sure our outbox content is sent (if the mainapp is still connected and also was in foreground while the sharesheet was used)
//and open the chat the newest outbox entry was sent to
//make sure activechats ui is properly initialized when calling this
createQueuedTimer(0.5, dispatch_get_main_queue(), (^{
DDLogInfo(@"Got %@ url, trying to send all outboxes...", kMonalOpenURL);
[self sendAllOutboxes];
}));
return YES;
}
return NO;
}
#pragma mark - user notifications
-(void) application:(UIApplication*) application didReceiveRemoteNotification:(NSDictionary*) userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result)) completionHandler
{
DDLogVerbose(@"got didReceiveRemoteNotification: %@", userInfo);
[self incomingWakeupWithCompletionHandler:completionHandler];
}
-(void) userNotificationCenter:(UNUserNotificationCenter*) center willPresentNotification:(UNNotification*) notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options)) completionHandler
{
DDLogInfo(@"userNotificationCenter:willPresentNotification:withCompletionHandler called");
//show local notifications while the app is open and ignore remote pushes
if([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
completionHandler(UNNotificationPresentationOptionNone);
} else {
completionHandler(UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner);
}
}
-(void) userNotificationCenter:(UNUserNotificationCenter*) center didReceiveNotificationResponse:(UNNotificationResponse*) response withCompletionHandler:(void (^)(void)) completionHandler
{
if([response.notification.request.content.categoryIdentifier isEqualToString:@"message"])
{
DDLogVerbose(@"notification action '%@' triggered for %@", response.actionIdentifier, response.notification.request.content.userInfo);
MLContact* fromContact = [MLContact createContactFromJid:response.notification.request.content.userInfo[@"fromContactJid"] andAccountID:response.notification.request.content.userInfo[@"fromContactAccountID"]];
MLAssert(fromContact, @"fromContact should not be nil");
NSString* messageId = response.notification.request.content.userInfo[@"messageId"];
MLAssert(messageId, @"messageId should not be nil");
xmpp* account = fromContact.account;
//this can happen if that account got disabled
if(account == nil)
{
//call completion handler directly (we did not handle anything and no connectIfNecessary was called)
if(completionHandler)
completionHandler();
return;
}
//add our completion handler to handler queue
[self incomingWakeupWithCompletionHandler:^(UIBackgroundFetchResult result __unused) {
completionHandler();
}];
//make sure we have an active buddy for this chat
[[DataLayer sharedInstance] addActiveBuddies:fromContact.contactJid forAccount:fromContact.accountID];
//handle message actions
if([response.actionIdentifier isEqualToString:@"REPLY_ACTION"])
{
DDLogInfo(@"REPLY_ACTION triggered...");
UNTextInputNotificationResponse* textResponse = (UNTextInputNotificationResponse*) response;
if(!textResponse.userText.length)
{
DDLogWarn(@"User tried to send empty text response!");
return;
}
//mark messages as read because we are replying
NSArray* unread = [[DataLayer sharedInstance] markMessagesAsReadForBuddy:fromContact.contactJid andAccount:fromContact.accountID tillStanzaId:messageId wasOutgoing:NO];
DDLogDebug(@"Marked as read: %@", unread);
//remove notifications of all read messages (this will cause the MLNotificationManager to update the app badge, too)
[[MLNotificationQueue currentQueue] postNotificationName:kMonalDisplayedMessagesNotice object:account userInfo:@{@"messagesArray":unread}];
//update unread count in active chats list
[fromContact refresh]; //this will make sure the unread count is correct
[[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{
@"contact": fromContact
}];
BOOL encrypted = [[DataLayer sharedInstance] shouldEncryptForJid:fromContact.contactJid andAccountID:fromContact.accountID];
[[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:textResponse.userText havingType:kMessageTypeText toContact:fromContact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) {
DDLogInfo(@"REPLY_ACTION success=%@, messageIdSentObject=%@", bool2str(successSendObject), messageIdSentObject);
}];
}
else if([response.actionIdentifier isEqualToString:@"MARK_AS_READ_ACTION"])
{
DDLogInfo(@"MARK_AS_READ_ACTION triggered...");
NSArray* unread = [[DataLayer sharedInstance] markMessagesAsReadForBuddy:fromContact.contactJid andAccount:fromContact.accountID tillStanzaId:messageId wasOutgoing:NO];
DDLogDebug(@"Marked as read: %@", unread);
//publish MDS display marker and optionally send displayed marker for last unread message (XEP-0333)
DDLogDebug(@"Sending MDS (and possibly XEP-0333 displayed marker) for messages: %@", unread);
[account sendDisplayMarkerForMessages:unread];
//remove notifications of all read messages (this will cause the MLNotificationManager to update the app badge, too)
[[MLNotificationQueue currentQueue] postNotificationName:kMonalDisplayedMessagesNotice object:account userInfo:@{@"messagesArray":unread}];
//update unread count in active chats list
[fromContact refresh]; //this will make sure the unread count is correct
[[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{
@"contact": fromContact
}];
}
else if([response.actionIdentifier isEqualToString:@"com.apple.UNNotificationDefaultActionIdentifier"]) //open chat of this contact
[self openChatOfContact:fromContact];
}
else if([response.notification.request.content.categoryIdentifier isEqualToString:@"subscription"])
{
DDLogVerbose(@"notification action '%@' triggered for %@", response.actionIdentifier, response.notification.request.content.userInfo);
MLContact* fromContact = [MLContact createContactFromJid:response.notification.request.content.userInfo[@"fromContactJid"] andAccountID:response.notification.request.content.userInfo[@"fromContactAccountID"]];
MLAssert(fromContact, @"fromContact should not be nil");
xmpp* account = fromContact.account;
//this can happen if that account got disabled
if(account == nil)
{
//call completion handler directly (we did not handle anything and no connectIfNecessary was called)
if(completionHandler)
completionHandler();
return;
}
//add our completion handler to handler queue
[self incomingWakeupWithCompletionHandler:^(UIBackgroundFetchResult result __unused) {
completionHandler();
}];
//handle subscription actions
if([response.actionIdentifier isEqualToString:@"APPROVE_SUBSCRIPTION_ACTION"])
{
DDLogInfo(@"APPROVE_SUBSCRIPTION_ACTION triggered...");
[[MLXMPPManager sharedInstance] addContact:fromContact];
[self openChatOfContact:fromContact];
}
else if([response.actionIdentifier isEqualToString:@"DENY_SUBSCRIPTION_ACTION"] || [response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier])
{
DDLogInfo(@"DENY_SUBSCRIPTION_ACTION triggered...");
[[MLXMPPManager sharedInstance] removeContact:fromContact];
}
else if([response.actionIdentifier isEqualToString:@"com.apple.UNNotificationDefaultActionIdentifier"]) //open chat of this contact
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
while(self.activeChats == nil)
usleep(100000);
dispatch_async(dispatch_get_main_queue(), ^{
[(ActiveChatsViewController*)self.activeChats showAddContact];
});
});
}
else
{
//call completion handler directly (we did not handle anything and no connectIfNecessary was called)
if(completionHandler)
completionHandler();
}
}
-(void) userNotificationCenter:(UNUserNotificationCenter*) center openSettingsForNotification:(UNNotification*) notification
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
while(self.activeChats == nil)
usleep(100000);
dispatch_async(dispatch_get_main_queue(), ^{
[(ActiveChatsViewController*)self.activeChats showNotificationSettings];
});
});
}
-(void) openChatOfContact:(MLContact* _Nullable) contact
{
return [self openChatOfContact:contact withCompletion:nil];
}
-(void) openChatOfContact:(MLContact* _Nullable) contact withCompletion:(monal_id_block_t _Nullable) completion
{
if(contact != nil)
_contactToOpen = contact;
if(completion != nil)
_completionToCall = completion;
if(self.activeChats != nil && _contactToOpen != nil)
{
// the timer makes sure the view is properly initialized when opning the chat
createQueuedTimer(0.5, dispatch_get_main_queue(), (^{
if(self->_contactToOpen != nil)
{
DDLogDebug(@"Opening chat for contact %@", [contact contactJid]);
// open new chat
[(ActiveChatsViewController*)self.activeChats presentChatWithContact:self->_contactToOpen andCompletion:self->_completionToCall];
}
else
DDLogDebug(@"_contactToOpen changed to nil, not opening chat for contact %@", [contact contactJid]);
self->_contactToOpen = nil;
self->_completionToCall = nil;
}));
}
else
DDLogDebug(@"Not opening chat for contact %@", [contact contactJid]);
}
-(UIInterfaceOrientationMask) application:(UIApplication*) application supportedInterfaceOrientationsForWindow:(UIWindow*) window
{
return self.orientationLock;
}
#pragma mark - memory
-(void) applicationDidReceiveMemoryWarning:(UIApplication*) application
{
DDLogWarn(@"Got memory warning!");
}
#pragma mark - backgrounding
-(void) startBackgroundTimer:(double) timeout
{
//cancel old background timer if still running and start a new one
//this timer will fire after timeout seconds in background and disconnect gracefully (e.g. when fully idle the next time)
if(_backgroundTimer)
_backgroundTimer();
_backgroundTimer = createTimer(timeout, ^{
//mark timer as *not* running
self->_backgroundTimer = nil;
//retry background check (now handling idle state because no running background timer is blocking it)
dispatch_async(dispatch_get_main_queue(), ^{
[self checkIfBackgroundTaskIsStillNeeded];
});
});
}
-(void) stopBackgroundTimer
{
if(_backgroundTimer)
_backgroundTimer();
_backgroundTimer = nil;
//stop bg processing/refreshing tasks (we are foregrounded now)
//this will prevent scenarious where one of these tasks times out after the user puts the app into background again
//in this case a possible syncError notification would be suppressed in checkIfBackgroundTaskIsStillNeeded
//but since the user openend the app, we want these errors not being suppressed
@synchronized(self) {
if(self->_bgProcessing != nil)
{
DDLogDebug(@"Stopping bg processing task, we are foregrounded now");
[DDLog flushLog];
BGTask* task = self->_bgProcessing;
self->_bgProcessing = nil;
[task setTaskCompletedWithSuccess:YES];
return;
}
}
@synchronized(self) {
if(self->_bgRefreshing != nil)
{
DDLogDebug(@"Stopping bg refreshing task, we are foregrounded now");
[DDLog flushLog];
BGTask* task = self->_bgRefreshing;
self->_bgRefreshing = nil;
[task setTaskCompletedWithSuccess:YES];
return;
}
}
}
-(UIViewController*) getTopViewController
{
UIViewController* topViewController = self.window.rootViewController;
while(topViewController.presentedViewController)
topViewController = topViewController.presentedViewController;
return topViewController;
}
-(void) prepareForFreeze:(NSNotification*) notification
{
for(xmpp* account in [MLXMPPManager sharedInstance].connectedXMPP)
[account freeze];
[MLProcessLock unlock];
_wasFreezed = YES;
@synchronized(self) {
DDLogVerbose(@"Setting _shutdownPending to NO...");
_shutdownPending = NO;
}
}
-(void) applicationWillEnterForeground:(UIApplication*) application
{
DDLogInfo(@"Entering FG");
[MLProcessLock lock];
@synchronized(self) {
DDLogVerbose(@"Setting _shutdownPending to NO...");
_shutdownPending = NO;
}
//only show loading HUD if we really got freezed before
MBProgressHUD* loadingHUD;
if(_wasFreezed)
{
loadingHUD = [MBProgressHUD showHUDAddedTo:[self getTopViewController].view animated:YES];
loadingHUD.label.text = NSLocalizedString(@"Refreshing...", @"");
loadingHUD.mode = MBProgressHUDModeIndeterminate;
loadingHUD.removeFromSuperViewOnHide = YES;
_wasFreezed = NO;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//make sure the progress HUD is displayed before freezing the main thread
//only proceed with foregrounding if the NotificationServiceExtension is not running
[[IPC sharedInstance] sendMessage:@"Monal.disconnectAll" withData:nil to:@"NotificationServiceExtension"];
if([MLProcessLock checkRemoteRunning:@"NotificationServiceExtension"])
{
DDLogInfo(@"NotificationServiceExtension is running, waiting for its termination");
[MLProcessLock waitForRemoteTermination:@"NotificationServiceExtension" withLoopHandler:^{
[[IPC sharedInstance] sendMessage:@"Monal.disconnectAll" withData:nil to:@"NotificationServiceExtension"];
}];
}
dispatch_async(dispatch_get_main_queue(), ^{
//cancel already running background timer, we are now foregrounded again
[self stopBackgroundTimer];
[self addBackgroundTask];
[[MLXMPPManager sharedInstance] nowForegrounded]; //NOTE: this will unfreeze all queues in our accounts
//open call ui using first call if at least one call is present
NSDictionary* activeCalls = [self.voipProcessor getActiveCalls];
for(NSUUID* uuid in activeCalls)
{
[self.activeChats presentCall:activeCalls[uuid]];
break;
}
//trigger view updates (this has to be done because the NotificationServiceExtension could have updated the database some time ago)
//this must be done *after* [[MLXMPPManager sharedInstance] nowForegrounded] to make sure an already open chat view
//knows it is now foregrounded (we obviously don't mark messages as read if a chat view is in background while still loaded/"visible")
[[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil];
if(loadingHUD != nil)
loadingHUD.hidden = YES;
});
});
}
-(void) nowReallyBackgrounded
{
[self addBackgroundTask];
[[MLXMPPManager sharedInstance] nowBackgrounded];
[self startBackgroundTimer:GRACEFUL_TIMEOUT];
dispatch_async(dispatch_get_main_queue(), ^{
[self checkIfBackgroundTaskIsStillNeeded];
});
}
-(void) applicationDidEnterBackground:(UIApplication*) application
{
UIApplicationState state = [application applicationState];
if(state == UIApplicationStateInactive)
DDLogInfo(@"Screen lock / incoming call");
else if(state == UIApplicationStateBackground)
DDLogInfo(@"Entering BG");
[self updateUnread];
#if TARGET_OS_MACCATALYST
if(NSProcessInfo.processInfo.isLowPowerModeEnabled)
{
DDLogInfo(@"LowPowerMode is active: nowReallyBackgrounded to reduce power consumption");
[self nowReallyBackgrounded];
}
else
[[MLXMPPManager sharedInstance] noLongerInFocus];
#else
[self nowReallyBackgrounded];
#endif
}
-(void) applicationWillTerminate:(UIApplication *)application
{
@synchronized(self) {
DDLogVerbose(@"Setting _shutdownPending to YES...");
_shutdownPending = YES;
DDLogWarn(@"|~~| T E R M I N A T I N G |~~|");
[HelperTools scheduleBackgroundTask:YES]; //make sure delivery will be attempted, if needed (force as soon as possible)
DDLogInfo(@"|~~| 33%% |~~|");
[[MLXMPPManager sharedInstance] nowBackgrounded];
DDLogInfo(@"|~~| 66%% |~~|");
[HelperTools updateSyncErrorsWithDeleteOnly:NO andWaitForCompletion:YES];
DDLogInfo(@"|~~| 99%% |~~|");
[[MLXMPPManager sharedInstance] disconnectAll];
DDLogInfo(@"|~~| T E R M I N A T E D |~~|");
[DDLog flushLog];
}
}
#pragma mark - error feedback
-(void) showConnectionStatus:(NSNotification*) notification
{
//this will show an error banner but only if our app is foregrounded
DDLogWarn(@"Got xmpp error %@", notification);
if(![HelperTools isNotInFocus])
{
dispatch_async(dispatch_get_main_queue(), ^{
xmpp* xmppAccount = notification.object;
//ignore errors with unknown accounts
//(possibly meaning an account we currently try to create --> the creating ui will take care of this already)
if(xmppAccount == nil)
return;
if(![notification.userInfo[@"isSevere"] boolValue])
DDLogError(@"Minor XMPP Error(%@): %@", xmppAccount.connectionProperties.identity.jid, notification.userInfo[@"message"]);
NotificationBanner* banner = [[NotificationBanner alloc] initWithTitle:xmppAccount.connectionProperties.identity.jid subtitle:notification.userInfo[@"message"] leftView:nil rightView:nil style:([notification.userInfo[@"isSevere"] boolValue] ? BannerStyleDanger : BannerStyleWarning) colors:nil];
banner.duration = 10.0; //show for 10 seconds to make sure users can read it
NotificationBannerQueue* queue = [[NotificationBannerQueue alloc] initWithMaxBannersOnScreenSimultaneously:2];
[banner showWithQueuePosition:QueuePositionBack bannerPosition:BannerPositionTop queue:queue on:nil];
});
}
else
DDLogWarn(@"Not showing error banner: app not in focus!");
}
#pragma mark - mac menu
-(void) buildMenuWithBuilder:(id<UIMenuBuilder>) builder
{
[super buildMenuWithBuilder:builder];
//monal
UIKeyCommand* preferencesCommand = [UIKeyCommand commandWithTitle:@"Preferences..." image:nil action:@selector(showSettings) input:@"," modifierFlags:UIKeyModifierCommand propertyList:nil];
UIMenu* preferencesMenu = [UIMenu menuWithTitle:@"" image:nil identifier:@"im.monal.preferences" options:UIMenuOptionsDisplayInline children:@[preferencesCommand]];
[builder insertSiblingMenu:preferencesMenu afterMenuForIdentifier:UIMenuAbout];
//file
UIKeyCommand* newCommand = [UIKeyCommand commandWithTitle:@"New Message" image:nil action:@selector(showNew) input:@"N" modifierFlags:UIKeyModifierCommand propertyList:nil];
UIMenu* newMenu = [UIMenu menuWithTitle:@"" image:nil identifier:@"im.monal.new" options:UIMenuOptionsDisplayInline children:@[newCommand]];
[builder insertChildMenu:newMenu atStartOfMenuForIdentifier:UIMenuFile];
UIKeyCommand* detailsCommand = [UIKeyCommand commandWithTitle:@"Details..." image:nil action:@selector(showDetails) input:@"I" modifierFlags:UIKeyModifierCommand propertyList:nil];
UIMenu* detailsMenu = [UIMenu menuWithTitle:@"" image:nil identifier:@"im.monal.detail" options:UIMenuOptionsDisplayInline children:@[detailsCommand]];
[builder insertSiblingMenu:detailsMenu afterMenuForIdentifier:@"im.monal.new"];
UIKeyCommand* deleteCommand = [UIKeyCommand commandWithTitle:@"Delete Conversation" image:nil action:@selector(deleteConversation) input:@"\b" modifierFlags:UIKeyModifierCommand propertyList:nil];
UIMenu* deleteMenu = [UIMenu menuWithTitle:@"" image:nil identifier:@"im.monal.delete" options:UIMenuOptionsDisplayInline children:@[deleteCommand]];
[builder insertSiblingMenu:deleteMenu afterMenuForIdentifier:@"im.monal.detail"];
[builder removeMenuForIdentifier:UIMenuHelp];
[builder replaceChildrenOfMenuForIdentifier:UIMenuAbout fromChildrenBlock:^NSArray<UIMenuElement *> * _Nonnull(NSArray<UIMenuElement *> * _Nonnull items) {
UICommand* itemCommand = (UICommand*)items.firstObject;
UICommand* aboutCommand = [UICommand commandWithTitle:itemCommand.title image:nil action:@selector(aboutWindow) propertyList:nil];
NSArray* menuItems = @[aboutCommand];
return menuItems;
}];
}
-(void) aboutWindow
{
UIStoryboard* settingStoryBoard = [UIStoryboard storyboardWithName:@"Settings" bundle:nil];
MLSettingsAboutViewController* settingAboutViewController = [settingStoryBoard instantiateViewControllerWithIdentifier:@"SettingsAboutViewController"];
UINavigationController* navigationController = [[UINavigationController alloc] initWithRootViewController:settingAboutViewController];
[self.window.rootViewController presentViewController:navigationController animated:NO completion:nil];
}
-(void) showNew
{
[self.activeChats showContacts];
}
-(void) deleteConversation
{
[self.activeChats deleteConversation];
}
-(void) showSettings
{
[self.activeChats showSettings];
}
-(void) showDetails
{
[self.activeChats showDetails];
}
#pragma mark - background tasks
-(void) handleSpinner
{
//show/hide spinner (dispatch *async* to main queue to allow for ui changes)
dispatch_async(dispatch_get_main_queue(), ^{
if(([[MLXMPPManager sharedInstance] allAccountsIdle] && [MLFiletransfer isIdle]))
[self.activeChats.spinner stopAnimating];
else
[self.activeChats.spinner startAnimating];
});
}
-(void) nowNotIdle:(NSNotification*) notification
{
DDLogInfo(@"### SOME ACCOUNT CHANGED TO NON-IDLE STATE ###");
[self handleSpinner];
}
-(void) nowIdle:(NSNotification*) notification
{
DDLogInfo(@"### SOME ACCOUNT CHANGED TO IDLE STATE ###");
[self handleSpinner];
//dispatch *async* to main queue to avoid deadlock between receiveQueue ---sync--> im.monal.disconnect ---sync--> receiveQueue
dispatch_async(dispatch_get_main_queue(), ^{
[self checkIfBackgroundTaskIsStillNeeded];
});
}
-(void) filetransfersNowIdle:(NSNotification*) notification
{
DDLogInfo(@"### FILETRANSFERS CHANGED TO IDLE STATE ###");
//dispatch *async* to main queue to avoid deadlock between receiveQueue ---sync--> im.monal.disconnect ---sync--> receiveQueue
dispatch_async(dispatch_get_main_queue(), ^{
[self checkIfBackgroundTaskIsStillNeeded];
});
}
//this method will either be called from an anonymous timer thread or from the main thread
-(void) checkIfBackgroundTaskIsStillNeeded
{
if([[MLXMPPManager sharedInstance] allAccountsIdle] && [MLFiletransfer isIdle])
{
DDLogInfo(@"### ALL ACCOUNTS IDLE AND FILETRANSFERS COMPLETE NOW ###");
//if we used a bg fetch/processing task, that means we did not get a push informing us about a waiting message
//nor did the user interact with our app --> don't show possible sync warnings in this case (but delete old warnings if we are synced now)
[HelperTools updateSyncErrorsWithDeleteOnly:(self->_bgProcessing != nil || self->_bgRefreshing != nil) andWaitForCompletion:YES];
//use a synchronized block to disconnect only once
@synchronized(self) {
if(_backgroundTimer != nil || [_wakeupCompletions count] > 0 || _voipProcessor.pendingCallsCount > 0)
{
DDLogInfo(@"### ignoring idle state because background timer or wakeup completion timers or pending calls are still running ###");
return;
}
if(_shutdownPending)
{
DDLogInfo(@"### ignoring idle state because a shutdown is already pending ###");
return;
}
DDLogInfo(@"### checking if background is still needed ###");
BOOL background = [HelperTools isInBackground];
if(background)
{
DDLogInfo(@"### All accounts idle, disconnecting and stopping all background tasks ###");
[DDLog flushLog];
DDLogVerbose(@"Setting _shutdownPending to YES...");
_shutdownPending = YES;
[[MLXMPPManager sharedInstance] disconnectAll]; //disconnect all accounts to prevent TCP buffer leaking
[HelperTools scheduleBackgroundTask:NO]; //request bg fetch execution in BGFETCH_DEFAULT_INTERVAL seconds
[HelperTools dispatchAsync:NO reentrantOnQueue:dispatch_get_main_queue() withBlock:^{
BOOL stopped = NO;
//make sure this will be done only once, even if we have an uikit bgtask and a bg fetch running simultaneously
if(self->_bgTask != UIBackgroundTaskInvalid || self->_bgProcessing != nil || self->_bgRefreshing != nil)
{
//notify about pending app freeze (don't queue this notification because it should be handled IMMEDIATELY and INLINE)
DDLogVerbose(@"Posting kMonalWillBeFreezed notification now...");
[[NSNotificationCenter defaultCenter] postNotificationName:kMonalWillBeFreezed object:nil];
}
if(self->_bgTask != UIBackgroundTaskInvalid)
{
DDLogDebug(@"stopping UIKit _bgTask");
[DDLog flushLog];
UIBackgroundTaskIdentifier task = self->_bgTask;
self->_bgTask = UIBackgroundTaskInvalid;
[[UIApplication sharedApplication] endBackgroundTask:task];
stopped = YES;
}
if(self->_bgProcessing != nil)
{
DDLogDebug(@"stopping backgroundProcessingTask");
[DDLog flushLog];
BGTask* task = self->_bgProcessing;
self->_bgProcessing = nil;
[task setTaskCompletedWithSuccess:YES];
stopped = YES;
}
if(self->_bgRefreshing != nil)
{
DDLogDebug(@"stopping backgroundRefreshingTask");
[DDLog flushLog];
BGTask* task = self->_bgRefreshing;
self->_bgRefreshing = nil;
[task setTaskCompletedWithSuccess:YES];
stopped = YES;
}
if(!stopped)
{
DDLogDebug(@"no background tasks running, nothing to stop");
[DDLog flushLog];
}
else
{
DDLogVerbose(@"Posting kMonalIsFreezed notification now...");
[[NSNotificationCenter defaultCenter] postNotificationName:kMonalIsFreezed object:nil];
[HelperTools flushLogsWithTimeout:0.100];
}
}];
}
}
}
}
-(void) addBackgroundTask
{
[HelperTools dispatchAsync:NO reentrantOnQueue:dispatch_get_main_queue() withBlock:^{
//don't start uikit bg task if it's already running
if(self->_bgTask != UIBackgroundTaskInvalid)
DDLogVerbose(@"Not starting UIKit background task, already running: %d", (int)self->_bgTask);
else
{
DDLogInfo(@"Starting UIKit background task...");
//indicate we want to do work even if the app is put into background
self->_bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void) {
DDLogWarn(@"BG WAKE EXPIRING");
[DDLog flushLog];
@synchronized(self) {
//ui background tasks expire at the same time as background processing/refreshing tasks
//--> we have to check if a background processing/refreshing task is running and don't disconnect, if so
BOOL stopped = NO;
if(self->_bgProcessing == nil && self->_bgRefreshing == nil)
{
DDLogVerbose(@"Setting _shutdownPending to YES...");
self->_shutdownPending = YES;
DDLogDebug(@"_bgProcessing == nil && _bgRefreshing == nil --> disconnecting and ending background task");
//this has to be before account disconnects, to detect which accounts are not idle (e.g. have a sync error)
[HelperTools updateSyncErrorsWithDeleteOnly:NO andWaitForCompletion:YES];
//disconnect all accounts to prevent TCP buffer leaking
[[MLXMPPManager sharedInstance] disconnectAll];
//schedule a BGProcessingTaskRequest to process this further as soon as possible
//(if we end up here, the graceful shuttdown did not work out because we are not idle --> we need more cpu time)
[HelperTools scheduleBackgroundTask:YES]; //force as soon as possible
//notify about pending app freeze (don't queue this notification because it should be handled IMMEDIATELY and INLINE)
DDLogVerbose(@"Posting kMonalWillBeFreezed notification now...");
[[NSNotificationCenter defaultCenter] postNotificationName:kMonalWillBeFreezed object:nil];
stopped = YES;
}
else
DDLogDebug(@"_bgProcessing != nil || _bgRefreshing != nil --> not disconnecting");
DDLogDebug(@"stopping UIKit _bgTask");
[DDLog flushLog];
UIBackgroundTaskIdentifier task = self->_bgTask;
self->_bgTask = UIBackgroundTaskInvalid;
[[UIApplication sharedApplication] endBackgroundTask:task];
if(stopped)
{
DDLogVerbose(@"Posting kMonalIsFreezed notification now...");
[[NSNotificationCenter defaultCenter] postNotificationName:kMonalIsFreezed object:nil];
[HelperTools flushLogsWithTimeout:0.100];
}
}
}];
}
}];
}
-(void) handleBackgroundProcessingTask:(BGTask*) task
{
DDLogInfo(@"RUNNING BGPROCESSING SETUP HANDLER");
_bgProcessing = task;
weakify(task);
task.expirationHandler = ^{
strongify(task);
DDLogWarn(@"*** BGPROCESSING EXPIRED ***");
[DDLog flushLog];
DDLogVerbose(@"Dispatching to main queue...");
[HelperTools dispatchAsync:NO reentrantOnQueue:dispatch_get_main_queue() withBlock:^{
BOOL background = [HelperTools isInBackground];
DDLogVerbose(@"Waiting for @synchronized(self)...");
@synchronized(self) {
DDLogVerbose(@"Now entered @synchronized(self) block...");
//ui background tasks expire at the same time as background fetching tasks
//--> we have to check if an ui bg task is running and don't disconnect, if so
BOOL stopped = NO;
if(background && self->_voipProcessor.pendingCallsCount == 0 && self->_bgTask == UIBackgroundTaskInvalid)
{
DDLogVerbose(@"Setting _shutdownPending to YES...");
self->_shutdownPending = YES;
DDLogDebug(@"_bgTask == UIBackgroundTaskInvalid --> disconnecting and ending background task");
//this has to be before account disconnects, to detect which accounts are not idle (e.g. have a sync error)
[HelperTools updateSyncErrorsWithDeleteOnly:YES andWaitForCompletion:YES];
//disconnect all accounts to prevent TCP buffer leaking
[[MLXMPPManager sharedInstance] disconnectAll];
//schedule a new BGProcessingTaskRequest to process this further as soon as possible
//(if we end up here, the graceful shuttdown did not work out because we are not idle --> we need more cpu time)
[HelperTools scheduleBackgroundTask:YES]; //force as soon as possible
//notify about pending app freeze (don't queue this notification because it should be handled IMMEDIATELY and INLINE)
DDLogVerbose(@"Posting kMonalWillBeFreezed notification now...");
[[NSNotificationCenter defaultCenter] postNotificationName:kMonalWillBeFreezed object:nil];
stopped = YES;
}
else
DDLogDebug(@"!background || _bgTask != UIBackgroundTaskInvalid --> not disconnecting");
DDLogDebug(@"stopping backgroundProcessingTask: %@", task);
[DDLog flushLog];
self->_bgProcessing = nil;
//only signal success, if we are not in background anymore (otherwise we *really* expired without being idle)
[task setTaskCompletedWithSuccess:!background];
if(stopped)
{
DDLogVerbose(@"Posting kMonalIsFreezed notification now...");
[[NSNotificationCenter defaultCenter] postNotificationName:kMonalIsFreezed object:nil];
[HelperTools flushLogsWithTimeout:0.100];
}
}
}];
};
//only proceed with our BGTASK if the NotificationServiceExtension is not running
[MLProcessLock lock];
[[IPC sharedInstance] sendMessage:@"Monal.disconnectAll" withData:nil to:@"NotificationServiceExtension"];
if([MLProcessLock checkRemoteRunning:@"NotificationServiceExtension"])
{
DDLogInfo(@"NotificationServiceExtension is running, waiting for its termination");
[MLProcessLock waitForRemoteTermination:@"NotificationServiceExtension" withLoopHandler:^{
[[IPC sharedInstance] sendMessage:@"Monal.disconnectAll" withData:nil to:@"NotificationServiceExtension"];
}];
}
//we allow ui bgtasks alongside "modern" bgtasks to extend our runtime in case the "modern" background tasks only provde a few seconds of bgtime
// if(self->_bgTask != UIBackgroundTaskInvalid)
// {
// DDLogDebug(@"stopping UIKit _bgTask, not needed when running a bg task");
// [DDLog flushLog];
// UIBackgroundTaskIdentifier task = self->_bgTask;
// self->_bgTask = UIBackgroundTaskInvalid;
// [[UIApplication sharedApplication] endBackgroundTask:task];
// }
if(self->_bgRefreshing != nil)
{
DDLogDebug(@"stopping bg refreshing task, not needed when running a (longer running) bg processing task");
[DDLog flushLog];
BGTask* refreshingTask = self->_bgRefreshing;
self->_bgRefreshing = nil;
[refreshingTask setTaskCompletedWithSuccess:YES];
}
if(![[MLXMPPManager sharedInstance] hasConnectivity])
DDLogError(@"BGTASK has *no* connectivity? That's strange!");
[self startBackgroundTimer:BGPROCESS_GRACEFUL_TIMEOUT];
@synchronized(self) {
DDLogVerbose(@"Setting _shutdownPending to NO...");
_shutdownPending = NO;
}
//don't use *self* connectIfNecessary, because we don't need an additional UIKit bg task, this one is already a bg task
[[MLXMPPManager sharedInstance] connectIfNecessary];
//request another execution in BGFETCH_DEFAULT_INTERVAL seconds
[HelperTools scheduleBackgroundTask:NO];
DDLogInfo(@"BGPROCESSING SETUP HANDLER COMPLETED SUCCESSFULLY...");
}
-(void) handleBackgroundRefreshingTask:(BGTask*) task
{
DDLogInfo(@"RUNNING BGREFRESHING SETUP HANDLER");
_bgRefreshing = task;
weakify(task);
task.expirationHandler = ^{
strongify(task);
DDLogWarn(@"*** BGREFRESHING EXPIRED ***");
[DDLog flushLog];
DDLogVerbose(@"Dispatching to main queue...");
[HelperTools dispatchAsync:NO reentrantOnQueue:dispatch_get_main_queue() withBlock:^{
BOOL background = [HelperTools isInBackground];
DDLogVerbose(@"Waiting for @synchronized(self)...");
@synchronized(self) {
DDLogVerbose(@"Now entered @synchronized(self) block...");
//ui background tasks expire at the same time as background fetching tasks
//--> we have to check if an ui bg task is running and don't disconnect, if so
BOOL stopped = NO;
if(background && self->_voipProcessor.pendingCallsCount == 0 && self->_bgTask == UIBackgroundTaskInvalid)
{
DDLogVerbose(@"Setting _shutdownPending to YES...");
self->_shutdownPending = YES;
DDLogDebug(@"_bgTask == UIBackgroundTaskInvalid --> disconnecting and ending background task");
//this has to be before account disconnects, to detect which accounts are not idle (e.g. have a sync error)
[HelperTools updateSyncErrorsWithDeleteOnly:YES andWaitForCompletion:YES];
//disconnect all accounts to prevent TCP buffer leaking
[[MLXMPPManager sharedInstance] disconnectAll];
//schedule a new BGProcessingTaskRequest to process this further as soon as possible
//(if we end up here, the graceful shuttdown did not work out because we are not idle --> we need more cpu time)
[HelperTools scheduleBackgroundTask:YES]; //force as soon as possible
//notify about pending app freeze (don't queue this notification because it should be handled IMMEDIATELY and INLINE)
DDLogVerbose(@"Posting kMonalWillBeFreezed notification now...");
[[NSNotificationCenter defaultCenter] postNotificationName:kMonalWillBeFreezed object:nil];
stopped = YES;
}
else
DDLogDebug(@"!background || _bgTask != UIBackgroundTaskInvalid --> not disconnecting");
DDLogDebug(@"stopping backgroundProcessingTask: %@", task);
[DDLog flushLog];
self->_bgRefreshing = nil;
//only signal success, if we are not in background anymore (otherwise we *really* expired without being idle)
[task setTaskCompletedWithSuccess:!background];
if(stopped)
{
DDLogVerbose(@"Posting kMonalIsFreezed notification now...");
[[NSNotificationCenter defaultCenter] postNotificationName:kMonalIsFreezed object:nil];
[HelperTools flushLogsWithTimeout:0.100];
}
}
}];
};
//only proceed with our BGTASK if the NotificationServiceExtension is not running
[MLProcessLock lock];
[[IPC sharedInstance] sendMessage:@"Monal.disconnectAll" withData:nil to:@"NotificationServiceExtension"];
if([MLProcessLock checkRemoteRunning:@"NotificationServiceExtension"])
{
DDLogInfo(@"NotificationServiceExtension is running, waiting for its termination");
[MLProcessLock waitForRemoteTermination:@"NotificationServiceExtension" withLoopHandler:^{
[[IPC sharedInstance] sendMessage:@"Monal.disconnectAll" withData:nil to:@"NotificationServiceExtension"];
}];
}
//we allow ui bgtasks alongside "modern" bgtasks to extend our runtime in case the "modern" background tasks only provde a few seconds of bgtime
// if(self->_bgTask != UIBackgroundTaskInvalid)
// {
// DDLogDebug(@"stopping UIKit _bgTask, not needed when running a bg task");
// [DDLog flushLog];
// UIBackgroundTaskIdentifier task = self->_bgTask;
// self->_bgTask = UIBackgroundTaskInvalid;
// [[UIApplication sharedApplication] endBackgroundTask:task];
// }
if(![[MLXMPPManager sharedInstance] hasConnectivity])
{
DDLogError(@"BGTASK has *no* connectivity? That's strange!");
}
[self startBackgroundTimer:GRACEFUL_TIMEOUT];
@synchronized(self) {
DDLogVerbose(@"Setting _shutdownPending to NO...");
_shutdownPending = NO;
}
//don't use *self* connectIfNecessary, because we don't need an additional UIKit bg task, this one is already a bg task
[[MLXMPPManager sharedInstance] connectIfNecessary];
//request another execution in BGFETCH_DEFAULT_INTERVAL seconds
[HelperTools scheduleBackgroundTask:NO];
DDLogInfo(@"BGREFRESHING SETUP HANDLER COMPLETED SUCCESSFULLY...");
}
-(void) configureBackgroundTasks
{
[[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:kBackgroundProcessingTask usingQueue:dispatch_get_main_queue() launchHandler:^(BGTask *task) {
DDLogDebug(@"RUNNING BGPROCESSING LAUNCH HANDLER");
DDLogInfo(@"BG time available: %f", [UIApplication sharedApplication].backgroundTimeRemaining);
if(![HelperTools isInBackground])
{
DDLogDebug(@"Already in foreground, stopping bgtask");
[task setTaskCompletedWithSuccess:YES];
return;
}
@synchronized(self) {
if(self->_bgProcessing != nil)
{
DDLogDebug(@"Already running a bg processing task, stopping second bg processing task");
[task setTaskCompletedWithSuccess:YES];
return;
}
}
[self handleBackgroundProcessingTask:task];
}];
[[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:kBackgroundRefreshingTask usingQueue:dispatch_get_main_queue() launchHandler:^(BGTask *task) {
DDLogDebug(@"RUNNING BGREFRESHING LAUNCH HANDLER");
DDLogInfo(@"BG time available: %f", [UIApplication sharedApplication].backgroundTimeRemaining);
if(![HelperTools isInBackground])
{
DDLogDebug(@"Already in foreground, stopping bgtask");
[task setTaskCompletedWithSuccess:YES];
return;
}
@synchronized(self) {
if(self->_bgProcessing != nil)
{
DDLogDebug(@"Already running bg processing task, stopping new bg refreshing task");
[task setTaskCompletedWithSuccess:YES];
return;
}
}
@synchronized(self) {
if(self->_bgRefreshing != nil)
{
DDLogDebug(@"Already running a bg refreshing task, stopping second bg refreshing task");
[task setTaskCompletedWithSuccess:YES];
return;
}
}
[self handleBackgroundRefreshingTask:task];
}];
}
-(void) handleScheduleBackgroundTaskNotification:(NSNotification*) notification
{
BOOL force = YES;
if(notification.userInfo)
force = [notification.userInfo[@"force"] boolValue];
[HelperTools scheduleBackgroundTask:force];
}
-(void) connectIfNecessaryWithOptions:(NSDictionary*) options
{
static NSUInteger applicationState;
static monal_void_block_t cancelEmergencyTimer;
static monal_void_block_t cancelCurrentTimer = nil;
static dispatch_once_t once;
dispatch_once(&once, ^{
applicationState = [UIApplication sharedApplication].applicationState;
cancelEmergencyTimer = createTimer(16.0, (^{
DDLogError(@"Emergency: crashlogs are still blocking connect after 16 seconds, connecting anyways!");
if(cancelCurrentTimer != nil)
cancelCurrentTimer();
[MLXMPPManager sharedInstance].isConnectBlocked = NO;
[[MLXMPPManager sharedInstance] connectIfNecessary];
}));
});
//this method is called by didFinishLaunchingWithOptions: and our ipc handler (but this is currently unused)
//we block the reconnect while the crash reports have not been processed yet, to avoid a crash loop preventing
//the user from sending the crash report
int count = [HelperTools pendingCrashreportCount];
if(count > 0 && options == nil && applicationState != UIApplicationStateBackground)
{
[MLXMPPManager sharedInstance].isConnectBlocked = YES;
DDLogWarn(@"Blocking connect of connectIfNecessary: crash reports still pending: %d, retrying in 1 second...", count);
cancelCurrentTimer = createTimer(1.0, (^{ [self connectIfNecessaryWithOptions:options]; }));
}
else
{
[MLXMPPManager sharedInstance].isConnectBlocked = NO;
DDLogInfo(@"Now unblocking connect of connectIfNecessary (applicationState%@UIApplicationStateBackground, count=%d, options=%@)...",
applicationState == UIApplicationStateBackground ? @"==" : @"!=",
count,
options
);
cancelEmergencyTimer();
}
[[MLXMPPManager sharedInstance] connectIfNecessary];
}
-(void) incomingWakeupWithCompletionHandler:(void (^)(UIBackgroundFetchResult result)) completionHandler
{
if(![HelperTools isInBackground])
{
DDLogWarn(@"Ignoring incomingWakeupWithCompletionHandler: because app is in FG!");
completionHandler(UIBackgroundFetchResultNoData);
return;
}
//we need the wakeup completion handling even if a uikit bgtask or bgprocessing or bgrefreshing is running because we want to keep
//the connection for a few seconds to allow message receipts to come in instead of triggering the appex
NSString* completionId = [[NSUUID UUID] UUIDString];
DDLogInfo(@"got incomingWakeupWithCompletionHandler with ID %@", completionId);
//only proceed with handling wakeup if the NotificationServiceExtension is not running
[MLProcessLock lock];
[[IPC sharedInstance] sendMessage:@"Monal.disconnectAll" withData:nil to:@"NotificationServiceExtension"];
if([MLProcessLock checkRemoteRunning:@"NotificationServiceExtension"])
{
DDLogInfo(@"NotificationServiceExtension is running, waiting for its termination");
[MLProcessLock waitForRemoteTermination:@"NotificationServiceExtension" withLoopHandler:^{
[[IPC sharedInstance] sendMessage:@"Monal.disconnectAll" withData:nil to:@"NotificationServiceExtension"];
}];
}
//don't use *self* connectIfNecessary] because we already have a background task here
//that gets stopped once we call the completionHandler
[[MLXMPPManager sharedInstance] connectIfNecessary];
//register push completion handler and associated timer (use the GRACEFUL_TIMEOUT here, too)
@synchronized(self) {
_wakeupCompletions[completionId] = @{
@"handler": completionHandler,
@"timer": createTimer(GRACEFUL_TIMEOUT, (^{
DDLogWarn(@"### Wakeup timer triggered for ID %@ ###", completionId);
dispatch_async(dispatch_get_main_queue(), ^{
@synchronized(self) {
DDLogInfo(@"Handling wakeup completion %@", completionId);
BOOL background = [HelperTools isInBackground];
//we have to check if an ui bg task or background processing/refreshing task is running and don't disconnect, if so
BOOL stopped = NO;
if(background && self->_voipProcessor.pendingCallsCount == 0 && self->_bgTask == UIBackgroundTaskInvalid && self->_bgProcessing == nil && self->_bgRefreshing == nil)
{
DDLogVerbose(@"Setting _shutdownPending to YES...");
self->_shutdownPending = YES;
DDLogDebug(@"background && _bgTask == UIBackgroundTaskInvalid && _bgProcessing == nil && _bgRefreshing == nil --> disconnecting and feeding wakeup completion");
//this has to be before account disconnects, to detect which accounts are/are not idle (e.g. don't have/have a sync error)
BOOL wasIdle = [[MLXMPPManager sharedInstance] allAccountsIdle] && [MLFiletransfer isIdle];
[HelperTools updateSyncErrorsWithDeleteOnly:NO andWaitForCompletion:YES];
//disconnect all accounts to prevent TCP buffer leaking
[[MLXMPPManager sharedInstance] disconnectAll];
//schedule a new BGProcessingTaskRequest to process this further as soon as possible, if we are not idle
//(if we end up here, the graceful shuttdown did not work out because we are not idle --> we need more cpu time)
[HelperTools scheduleBackgroundTask:!wasIdle];
//notify about pending app freeze (don't queue this notification because it should be handled IMMEDIATELY and INLINE)
DDLogVerbose(@"Posting kMonalWillBeFreezed notification now...");
[[NSNotificationCenter defaultCenter] postNotificationName:kMonalWillBeFreezed object:nil];
stopped = YES;
}
else
DDLogDebug(@"NOT (background && _bgTask == UIBackgroundTaskInvalid && _bgProcessing == nil && _bgRefreshing == nil) --> not disconnecting");
//call completion (should be done *after* the idle state check because it could freeze the app)
DDLogInfo(@"Calling wakeup completion handler...");
[DDLog flushLog];
[self->_wakeupCompletions removeObjectForKey:completionId];
completionHandler(UIBackgroundFetchResultFailed);
if(stopped)
{
DDLogVerbose(@"Posting kMonalIsFreezed notification now...");
[[NSNotificationCenter defaultCenter] postNotificationName:kMonalIsFreezed object:nil];
[HelperTools flushLogsWithTimeout:0.100];
}
//trigger disconnect if we are idle and no timer is blocking us now
if(self->_bgTask != UIBackgroundTaskInvalid || self->_bgProcessing != nil || self->_bgRefreshing != nil)
dispatch_async(dispatch_get_main_queue(), ^{
[self checkIfBackgroundTaskIsStillNeeded];
});
}
});
}))
};
DDLogInfo(@"Added timer %@ to wakeup completion list...", completionId);
}
}
#pragma mark - share sheet added
//send all sharesheet outboxes (this method will be called by AppDelegate if opened via monalOpen:// url)
-(void) sendAllOutboxes
{
//delay outbox sending until we have an active chats ui
if(self.activeChats == nil)
{
createQueuedTimer(0.5, dispatch_get_main_queue(), (^{
[self sendAllOutboxes];
}));
return;
}
[(ActiveChatsViewController*)self.activeChats dismissCompleteViewChainWithAnimation:YES andCompletion:^{
//open the destination chat only once
for(NSDictionary* payload in [[DataLayer sharedInstance] getShareSheetPayload])
{
DDLogInfo(@"Sending outbox entry: %@", payload);
xmpp* account = [[MLXMPPManager sharedInstance] getEnabledAccountForID:payload[@"account_id"]];
if(account == nil)
{
UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Sharing failed", @"") message:[NSString stringWithFormat:NSLocalizedString(@"Cannot share something with disabled/deleted account, destination: %@, internal account id: %@", @""), payload[@"recipient"], payload[@"account_id"]] preferredStyle:UIAlertControllerStyleAlert];
[messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) {
}]];
[self.activeChats presentViewController:messageAlert animated:YES completion:nil];
[[DataLayer sharedInstance] deleteShareSheetPayloadWithId:payload[@"id"]];
continue;
}
MLContact* contact = [MLContact createContactFromJid:payload[@"recipient"] andAccountID:account.accountID];
monal_id_block_t cleanup = ^(NSDictionary* payload) {
[[DataLayer sharedInstance] deleteShareSheetPayloadWithId:payload[@"id"]];
[[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil];
if(self.activeChats.currentChatView != nil)
{
[self.activeChats.currentChatView scrollToBottomAnimated:NO];
[self.activeChats.currentChatView hideUploadHUD];
}
//send next item (if there is one left)
[self sendAllOutboxes];
};
monal_id_block_t sendItem = ^(id dummy __unused){
BOOL encrypted = [[DataLayer sharedInstance] shouldEncryptForJid:contact.contactJid andAccountID:contact.accountID];
if([payload[@"type"] isEqualToString:@"text"])
{
[[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeText toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) {
DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountID, messageIdSentObject);
cleanup(payload);
}];
}
else if([payload[@"type"] isEqualToString:@"url"])
{
[[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeUrl toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) {
DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountID, messageIdSentObject);
cleanup(payload);
}];
}
else if([payload[@"type"] isEqualToString:@"geo"])
{
[[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:payload[@"data"] havingType:kMessageTypeGeo toContact:contact isEncrypted:encrypted uploadInfo:nil withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) {
DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountID, messageIdSentObject);
cleanup(payload);
}];
}
else if([payload[@"type"] isEqualToString:@"image"] || [payload[@"type"] isEqualToString:@"file"] || [payload[@"type"] isEqualToString:@"contact"] || [payload[@"type"] isEqualToString:@"audiovisual"])
{
DDLogInfo(@"Got %@ upload: %@", payload[@"type"], payload[@"data"]);
[self.activeChats.currentChatView showUploadHUD];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
$call(payload[@"data"], $ID(account), $BOOL(encrypted), $ID(completion, (^(NSString* url, NSString* mimeType, NSNumber* size, NSError* error) {
dispatch_async(dispatch_get_main_queue(), ^{
if(error != nil)
{
DDLogError(@"Failed to upload outbox file: %@", error);
NSMutableDictionary* payloadCopy = [NSMutableDictionary dictionaryWithDictionary:payload];
cleanup(payloadCopy);
UIAlertController* messageAlert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Failed to share file", @"") message:[NSString stringWithFormat:NSLocalizedString(@"Error: %@", @""), error] preferredStyle:UIAlertControllerStyleAlert];
[messageAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleCancel handler:^(UIAlertAction* action __unused) {
}]];
[self.activeChats presentViewController:messageAlert animated:YES completion:nil];
}
else
[[MLXMPPManager sharedInstance] sendMessageAndAddToHistory:url havingType:kMessageTypeFiletransfer toContact:contact isEncrypted:encrypted uploadInfo:@{@"mimeType": mimeType, @"size": size} withCompletionHandler:^(BOOL successSendObject, NSString* messageIdSentObject) {
DDLogInfo(@"SHARESHEET_SEND_DATA success=%@, account=%@, messageIdSentObject=%@", bool2str(successSendObject), account.accountID, messageIdSentObject);
cleanup(payload);
}];
});
})));
});
}
else
unreachable(@"Outbox payload type unknown", payload);
};
DDLogVerbose(@"Trying to open chat of outbox receiver: %@", contact);
[[DataLayer sharedInstance] addActiveBuddies:contact.contactJid forAccount:contact.accountID];
//don't use [self openChatOfContact:withCompletion:] because it's asynchronous and can only handle one contact at a time (e.g. until the asynchronous execution finished)
//we can invoke the activeChats interface directly instead, because we already did the necessary preparations ourselves
[(ActiveChatsViewController*)self.activeChats presentChatWithContact:contact andCompletion:sendItem];
//only send one item at a time (this method will be invoked again when sending completed)
break;
}
}];
}
@end