// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2015-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #import "RootNavigationController.h" #import #import #import "ContactGroupPickerViewController.h" #import "Contact.h" #import "GroupProxy.h" #import "ServerConnector.h" #import "DatabaseManager.h" #import "MyIdentityStore.h" #import "BundleUtil.h" #import "KKPasscodeLock.h" #import "TouchIdAuthentication.h" #import "NibUtil.h" #import "RectUtil.h" #import "ProgressViewController.h" #import "AppGroup.h" #import "MessageQueue.h" #import "UserSettings.h" #import "ModalNavigationController.h" #import "UTIConverter.h" #import "Utils.h" #import "FeatureMask.h" #import "LicenseStore.h" #import "JKLLockScreenViewController.h" #import "DocumentManager.h" #import "SenderItemManager.h" #define MAX_NUM_PASSCODE_TRIES 3 #ifdef DEBUG static const DDLogLevel ddLogLevel = DDLogLevelAll; #else static const DDLogLevel ddLogLevel = DDLogLevelWarning; #endif @interface RootNavigationController () @property NSMutableSet *recipientConversations; @property SenderItemManager *itemManager; @property NSInteger passcodeTryCount; @property JKLLockScreenViewController *passcodeVC; @property ProgressViewController *progressViewController; @property BOOL isAuthorized; @end @implementation RootNavigationController - (void)viewDidLoad { [super viewDidLoad]; _passcodeTryCount = 0; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [AppGroup setGroupId:THREEMA_GROUP_IDENTIFIER]; [AppGroup setAppId:APP_ID]; // Initialize app setup state (checking database file exists) as early as possible (void)[[AppSetupState alloc] init]; }); [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [Colors updateNavigationBar:self.navigationBar]; // drop shared instance in order to adapt to user's configuration changes [UserSettings resetSharedInstance]; [AppGroup setActive:YES forType:AppGroupTypeShareExtension]; #ifdef DEBUG [LogManager initializeGlobalLoggerWithDebug:YES]; #else [LogManager initializeGlobalLoggerWithDebug:NO]; #endif if ([self extensionIsReady]) { [self presentSharingUI]; } else { ;//nop } dispatch_async(dispatch_get_main_queue(), ^{ [self loadItemsFromContext]; }); } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; } - (void)presentSharingUI { // Add received pushs into db [[ServerConnector sharedServerConnector] connect]; ModalNavigationController *navigationController = [ContactGroupPickerViewController pickerFromStoryboardWithDelegate:self]; navigationController.navigationBar.translucent = NO; [self presentViewController:navigationController animated:YES completion:^{ //nop }]; } - (BOOL)isDBReady { DatabaseManager *dbManager = [DatabaseManager dbManager]; if ([dbManager storeRequiresMigration]) { [self showNeedStartAppFirst]; return NO; } return YES; } - (BOOL)hasLicense { if ([[LicenseStore sharedLicenseStore] isValid] == NO) { [self showNeedStartAppFirst]; return NO; } return YES; } - (void)showNeedStartAppFirst { NSString *title = NSLocalizedString(@"need_to_start_app_first_title", nil); NSString *message = NSLocalizedString(@"need_to_start_app_first_message", nil); [self showAlertWithTitle:title message:message closeOnOk:YES]; } - (BOOL)checkPasscode { NSUserDefaults *defaults = [AppGroup userDefaults]; time_t openTime = [defaults doubleForKey:@"UIActivityViewControllerOpenTime"]; BOOL hidePasslock = NO; int maxTimeSinceApp = 10; time_t uptime = [Utils systemUptime]; if (uptime > 0 && openTime > 0 && (uptime - openTime) > 0 && (uptime - openTime) < maxTimeSinceApp) { hidePasslock = YES; } [defaults removeObjectForKey:@"UIActivityViewControllerOpenTime"]; if (([[KKPasscodeLock sharedLock] isPasscodeRequired] && [[KKPasscodeLock sharedLock] isWithinGracePeriod] == NO) && !hidePasslock) { _isAuthorized = NO; JKLLockScreenViewController *vc = [[JKLLockScreenViewController alloc] initWithNibName:NSStringFromClass([JKLLockScreenViewController class]) bundle:[BundleUtil frameworkBundle]]; vc.lockScreenMode = LockScreenModeExtension; vc.delegate = self; UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc]; nav.navigationBarHidden = YES; [self presentViewController:nav animated:YES completion:^{ [self tryTouchIdAuthentication]; }]; return NO; } return YES; } - (void)tryTouchIdAuthentication { [TouchIdAuthentication tryTouchIdAuthenticationCallback:^(BOOL success, NSError *error) { if (success) { dispatch_async(dispatch_get_main_queue(), ^{ [self.presentedViewController dismissViewControllerAnimated:YES completion:^{ [self presentSharingUI]; }]; }); } }]; } - (BOOL)checkContextItems { if ([self.extensionContext.inputItems count] == 0) { NSString *title = NSLocalizedString(@"error_message_no_items_title", nil); NSString *message = NSLocalizedString(@"error_message_no_items_message", nil); [self showAlertWithTitle:title message:message closeOnOk:YES]; return NO; } return YES; } - (BOOL)extensionIsReady { // drop shared instance, otherwise we won't notice any changes to it [MyIdentityStore resetSharedInstance]; AppSetupState *appSetupSate = [[AppSetupState alloc] initWithMyIdentityStore:[MyIdentityStore sharedMyIdentityStore]]; if (![appSetupSate isAppSetupCompleted]) { [self showNeedStartAppFirst]; return NO; } if ([self hasLicense] == NO) { return NO; } if ([self isDBReady] == NO) { return NO; } if ([self checkPasscode] == NO) { return NO; } if ([self checkContextItems] == NO) { return NO; } return YES; } - (void)showAlertWithTitle:(NSString *)title message:(NSString *)message closeOnOk:(BOOL)closeOnOk { [UIAlertTemplate showAlertWithOwner:self.presentedViewController title:title message:message actionOk:^(UIAlertAction * _Nonnull okAction) { if (closeOnOk) { [self.extensionContext completeRequestReturningItems:@[] completionHandler:^(BOOL expired) { [self commonCompletionHandler]; }]; } }]; } - (void)showAlert:(UIAlertController *)alertController { if (self.presentedViewController) { [self.presentedViewController presentViewController:alertController animated:YES completion:nil]; } else { [self presentViewController:alertController animated:YES completion:nil]; } } - (void)loadItemsFromContext { _itemManager = [[SenderItemManager alloc] init]; _itemManager.delegate = self; for (NSExtensionItem *item in self.extensionContext.inputItems) { for (NSItemProvider *itemProvider in item.attachments) { NSString *baseUTI = [self getBaseUTIType:itemProvider]; NSString *secondUTI = [self getSecondUTIType:itemProvider]; [_itemManager addItem:itemProvider forType:baseUTI secondType:secondUTI]; } } } - (NSString *)getBaseUTIType:(NSItemProvider *)itemProvider { NSMutableArray *typeIdentifiers = [NSMutableArray arrayWithArray:itemProvider.registeredTypeIdentifiers]; if (@available(iOS 13.0, *)) { if ([typeIdentifiers count] >= 1) { return [typeIdentifiers lastObject]; } return UTTYPE_FILE_URL; } else { if ([itemProvider hasItemConformingToTypeIdentifier:UTTYPE_FILE_URL]) { [typeIdentifiers removeObject:UTTYPE_FILE_URL]; } // take the first type if ([typeIdentifiers count] >= 1) { return [typeIdentifiers firstObject]; } return UTTYPE_FILE_URL; } } - (NSString *)getSecondUTIType:(NSItemProvider *)itemProvider { NSMutableArray *typeIdentifiers = [NSMutableArray arrayWithArray:itemProvider.registeredTypeIdentifiers]; if (@available(iOS 13.0, *)) { if ([itemProvider hasItemConformingToTypeIdentifier:UTTYPE_FILE_URL]) { [typeIdentifiers removeObject:UTTYPE_FILE_URL]; } if ([typeIdentifiers count] >= 1) { return [typeIdentifiers firstObject]; } return UTTYPE_FILE_URL; } else { return nil; } } - (BOOL)canConnect { return [ServerConnector sharedServerConnector].connectionState == ConnectionStateLoggedIn; } - (void)showProgressUI { _progressViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"ProgressViewController"]; _progressViewController.delegate = self; _progressViewController.totalCount = _itemManager.itemCount * [_recipientConversations count]; self.presentedViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; _progressViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; if ([self.presentedViewController isKindOfClass:[ModalNavigationController class]]) { ((ModalNavigationController *)self.presentedViewController).modalDelegate = nil; } [self dismissViewControllerAnimated:YES completion:^{ [self presentViewController:_progressViewController animated:YES completion:^{ [self sendItems]; }]; }]; } - (void)checkFeatureMaskAndSendItems { if ([_itemManager containsFileItem]) { [FeatureMask checkFeatureMask:FEATURE_MASK_FILE_TRANSFER forConversations:_recipientConversations onCompletion:^(NSArray *unsupportedContacts) { if ([unsupportedContacts count] > 0) { [self recipientConversationsRemoveContacts:unsupportedContacts]; NSString *messageFormat; if ([_recipientConversations count] == 0) { messageFormat = [BundleUtil localizedStringForKey:@"error_message_none_feature_level"]; } else { messageFormat = [BundleUtil localizedStringForKey:@"error_message_feature_level"]; } NSString *participantNames = [Utils stringFromContacts:unsupportedContacts]; NSString *message = [NSString stringWithFormat:messageFormat, participantNames]; NSString *title = [BundleUtil localizedStringForKey:@"error_title_feature_level"]; [UIAlertTemplate showAlertWithOwner:self title:title message:message actionOk:^(UIAlertAction * _Nonnull okAction) { [self startSending]; }]; } else { [self startSending]; } }]; } else { [self startSending]; } } - (void)recipientConversationsRemoveContacts:(NSArray *)contacts { NSMutableSet *newRecipientConversations = [NSMutableSet setWithSet:_recipientConversations]; for (Contact *contact in contacts) { for (Conversation *conversation in _recipientConversations) { if (conversation.isGroup) { if ([conversation.members isEqualToSet:[NSSet setWithArray:contacts]]) { [newRecipientConversations removeObject:conversation]; } } else if (conversation.contact == contact) { [newRecipientConversations removeObject:conversation]; } } } _recipientConversations = newRecipientConversations; } - (void)sendItems { [_itemManager sendItemsTo:_recipientConversations]; } - (void)startSending { NSInteger count = [_recipientConversations count] * _itemManager.itemCount; if (count == 0) { if (_itemManager.itemCount == 0) { // exit only if no items to send, otherwise the user has the chance to select another recipient [self finishAndClose]; } return; } if ([self canConnect] == NO) { NSString *title = NSLocalizedString(@"cannot_connect_title", nil); NSString *message = NSLocalizedString(@"cannot_connect_message", nil); [self showAlertWithTitle:title message:message closeOnOk:NO]; return; } dispatch_async(dispatch_get_main_queue(), ^{ [self showProgressUI]; }); } - (void)commonCompletionHandler { [AppGroup setActive:NO forType:AppGroupTypeShareExtension]; } - (void)completionHandler:(BOOL)expired { [self commonCompletionHandler]; if (expired) { _itemManager.shouldCancel = YES; [[ServerConnector sharedServerConnector] disconnect]; } else { [[ServerConnector sharedServerConnector] disconnectWait]; } } - (void)finishAndClose { [AppGroup setActive:NO forType:AppGroupTypeShareExtension]; [[MessageQueue sharedMessageQueue] save]; NSInteger delay = 0; if (_progressViewController != nil) { // show progress for long enough & give server connection enough time to handle acks delay = 1; } dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.extensionContext completeRequestReturningItems:@[] completionHandler:^(BOOL expired) { [self completionHandler:expired]; }]; }); } #pragma mark - ContactGroupPickerDelegate - (void)contactPicker:(ContactGroupPickerViewController*)contactPicker didPickConversations:(NSSet *)conversations renderType:(NSNumber *)renderType sendAsFile:(BOOL)sendAsFile { _recipientConversations = [NSMutableSet set]; _itemManager.sendAsFile = sendAsFile; if (contactPicker.additionalTextToSend) { [_itemManager addText:contactPicker.additionalTextToSend]; } for (Conversation *conversation in conversations) { [_recipientConversations addObject:conversation]; } [self checkFeatureMaskAndSendItems]; } - (void)contactPickerDidCancel:(ContactGroupPickerViewController*)contactPicker { [self finishAndClose]; } #pragma mark - Passcode lock delegate - (void)shouldEraseApplicationData:(JKLLockScreenViewController *)viewController { // do not delete stuff from within extension, just quit [self.extensionContext completeRequestReturningItems:@[] completionHandler:^(BOOL expired) { [self commonCompletionHandler]; }]; } - (void)didPasscodeEnteredIncorrectly:(JKLLockScreenViewController *)viewController { if (_passcodeTryCount >= MAX_NUM_PASSCODE_TRIES) { [self.extensionContext completeRequestReturningItems:@[] completionHandler:^(BOOL expired) { [self commonCompletionHandler]; }]; } _passcodeTryCount++; } - (void)didPasscodeEnteredCorrectly:(JKLLockScreenViewController *)viewController { _isAuthorized = YES; } - (void)unlockWasCancelledLockScreenViewController:(JKLLockScreenViewController *)lockScreenViewController { [self finishAndClose]; } - (void)didPasscodeViewDismiss:(JKLLockScreenViewController *)viewController { if (_isAuthorized) { [self presentSharingUI]; } } #pragma mark - ProgressViewDelegate - (void)progressViewDidCancel { _itemManager.shouldCancel = YES; dispatch_async(dispatch_get_main_queue(), ^{ [self finishAndClose]; }); } #pragma mark - ModalNavigationControllerDelegate - (void)willDismissModalNavigationController { [self finishAndClose]; } #pragma mark - SenderItemDelegate - (void)showAlertWithTitle:(NSString *)title message:(NSString *)message { dispatch_async(dispatch_get_main_queue(), ^{ [self showAlertWithTitle:title message:message closeOnOk:YES]; }); } - (void)finishedItem:(id)item { [_progressViewController finishedItem:item]; } - (void)setProgress:(NSNumber *)progress forItem:(id)item { [_progressViewController setProgress:progress forItem:item]; } - (void)setFinished { [self finishAndClose]; } #pragma mark UIApplicationDelegate - (void)didBecomeActive:(NSNotification*)notification { [AppGroup setActive:YES forType:AppGroupTypeShareExtension]; } @end