RootNavigationController.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2015-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import "RootNavigationController.h"
  21. #import <MobileCoreServices/MobileCoreServices.h>
  22. #import <ThreemaFramework/ThreemaFramework-Swift.h>
  23. #import "ContactGroupPickerViewController.h"
  24. #import "Contact.h"
  25. #import "GroupProxy.h"
  26. #import "ServerConnector.h"
  27. #import "DatabaseManager.h"
  28. #import "MyIdentityStore.h"
  29. #import "BundleUtil.h"
  30. #import "KKPasscodeLock.h"
  31. #import "TouchIdAuthentication.h"
  32. #import "NibUtil.h"
  33. #import "RectUtil.h"
  34. #import "ProgressViewController.h"
  35. #import "AppGroup.h"
  36. #import "MessageQueue.h"
  37. #import "UserSettings.h"
  38. #import "ModalNavigationController.h"
  39. #import "UTIConverter.h"
  40. #import "Utils.h"
  41. #import "FeatureMask.h"
  42. #import "LicenseStore.h"
  43. #import "JKLLockScreenViewController.h"
  44. #import "DocumentManager.h"
  45. #import "SenderItemManager.h"
  46. #define MAX_NUM_PASSCODE_TRIES 3
  47. #ifdef DEBUG
  48. static const DDLogLevel ddLogLevel = DDLogLevelAll;
  49. #else
  50. static const DDLogLevel ddLogLevel = DDLogLevelWarning;
  51. #endif
  52. @interface RootNavigationController () <ContactGroupPickerDelegate, KKPasscodeViewControllerDelegate, JKLLockScreenViewControllerDelegate, ProgressViewDelegate, ModalNavigationControllerDelegate, SenderItemDelegate>
  53. @property NSMutableSet *recipientConversations;
  54. @property SenderItemManager *itemManager;
  55. @property NSInteger passcodeTryCount;
  56. @property JKLLockScreenViewController *passcodeVC;
  57. @property ProgressViewController *progressViewController;
  58. @property BOOL isAuthorized;
  59. @end
  60. @implementation RootNavigationController
  61. - (void)viewDidLoad {
  62. [super viewDidLoad];
  63. _passcodeTryCount = 0;
  64. static dispatch_once_t onceToken;
  65. dispatch_once(&onceToken, ^{
  66. [AppGroup setGroupId:THREEMA_GROUP_IDENTIFIER];
  67. [AppGroup setAppId:APP_ID];
  68. // Initialize app setup state (checking database file exists) as early as possible
  69. (void)[[AppSetupState alloc] init];
  70. });
  71. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
  72. }
  73. - (void)viewWillAppear:(BOOL)animated {
  74. [super viewWillAppear:animated];
  75. [Colors updateNavigationBar:self.navigationBar];
  76. // drop shared instance in order to adapt to user's configuration changes
  77. [UserSettings resetSharedInstance];
  78. [AppGroup setActive:YES forType:AppGroupTypeShareExtension];
  79. #ifdef DEBUG
  80. [LogManager initializeGlobalLoggerWithDebug:YES];
  81. #else
  82. [LogManager initializeGlobalLoggerWithDebug:NO];
  83. #endif
  84. if ([self extensionIsReady]) {
  85. [self presentSharingUI];
  86. } else {
  87. ;//nop
  88. }
  89. dispatch_async(dispatch_get_main_queue(), ^{
  90. [self loadItemsFromContext];
  91. });
  92. }
  93. - (void)viewDidAppear:(BOOL)animated {
  94. [super viewDidAppear:animated];
  95. }
  96. - (void)presentSharingUI {
  97. // Add received pushs into db
  98. [[ServerConnector sharedServerConnector] connect];
  99. ModalNavigationController *navigationController = [ContactGroupPickerViewController pickerFromStoryboardWithDelegate:self];
  100. navigationController.navigationBar.translucent = NO;
  101. [self presentViewController:navigationController animated:YES completion:^{
  102. //nop
  103. }];
  104. }
  105. - (BOOL)isDBReady {
  106. DatabaseManager *dbManager = [DatabaseManager dbManager];
  107. if ([dbManager storeRequiresMigration]) {
  108. [self showNeedStartAppFirst];
  109. return NO;
  110. }
  111. return YES;
  112. }
  113. - (BOOL)hasLicense {
  114. if ([[LicenseStore sharedLicenseStore] isValid] == NO) {
  115. [self showNeedStartAppFirst];
  116. return NO;
  117. }
  118. return YES;
  119. }
  120. - (void)showNeedStartAppFirst {
  121. NSString *title = NSLocalizedString(@"need_to_start_app_first_title", nil);
  122. NSString *message = NSLocalizedString(@"need_to_start_app_first_message", nil);
  123. [self showAlertWithTitle:title message:message closeOnOk:YES];
  124. }
  125. - (BOOL)checkPasscode {
  126. NSUserDefaults *defaults = [AppGroup userDefaults];
  127. time_t openTime = [defaults doubleForKey:@"UIActivityViewControllerOpenTime"];
  128. BOOL hidePasslock = NO;
  129. int maxTimeSinceApp = 10;
  130. time_t uptime = [Utils systemUptime];
  131. if (uptime > 0 && openTime > 0 && (uptime - openTime) > 0 && (uptime - openTime) < maxTimeSinceApp) {
  132. hidePasslock = YES;
  133. }
  134. [defaults removeObjectForKey:@"UIActivityViewControllerOpenTime"];
  135. if (([[KKPasscodeLock sharedLock] isPasscodeRequired] && [[KKPasscodeLock sharedLock] isWithinGracePeriod] == NO) && !hidePasslock) {
  136. _isAuthorized = NO;
  137. JKLLockScreenViewController *vc = [[JKLLockScreenViewController alloc] initWithNibName:NSStringFromClass([JKLLockScreenViewController class]) bundle:[BundleUtil frameworkBundle]];
  138. vc.lockScreenMode = LockScreenModeExtension;
  139. vc.delegate = self;
  140. UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
  141. nav.navigationBarHidden = YES;
  142. [self presentViewController:nav animated:YES completion:^{
  143. [self tryTouchIdAuthentication];
  144. }];
  145. return NO;
  146. }
  147. return YES;
  148. }
  149. - (void)tryTouchIdAuthentication {
  150. [TouchIdAuthentication tryTouchIdAuthenticationCallback:^(BOOL success, NSError *error) {
  151. if (success) {
  152. dispatch_async(dispatch_get_main_queue(), ^{
  153. [self.presentedViewController dismissViewControllerAnimated:YES completion:^{
  154. [self presentSharingUI];
  155. }];
  156. });
  157. }
  158. }];
  159. }
  160. - (BOOL)checkContextItems {
  161. if ([self.extensionContext.inputItems count] == 0) {
  162. NSString *title = NSLocalizedString(@"error_message_no_items_title", nil);
  163. NSString *message = NSLocalizedString(@"error_message_no_items_message", nil);
  164. [self showAlertWithTitle:title message:message closeOnOk:YES];
  165. return NO;
  166. }
  167. return YES;
  168. }
  169. - (BOOL)extensionIsReady {
  170. // drop shared instance, otherwise we won't notice any changes to it
  171. [MyIdentityStore resetSharedInstance];
  172. AppSetupState *appSetupSate = [[AppSetupState alloc] initWithMyIdentityStore:[MyIdentityStore sharedMyIdentityStore]];
  173. if (![appSetupSate isAppSetupCompleted]) {
  174. [self showNeedStartAppFirst];
  175. return NO;
  176. }
  177. if ([self hasLicense] == NO) {
  178. return NO;
  179. }
  180. if ([self isDBReady] == NO) {
  181. return NO;
  182. }
  183. if ([self checkPasscode] == NO) {
  184. return NO;
  185. }
  186. if ([self checkContextItems] == NO) {
  187. return NO;
  188. }
  189. return YES;
  190. }
  191. - (void)showAlertWithTitle:(NSString *)title message:(NSString *)message closeOnOk:(BOOL)closeOnOk {
  192. [UIAlertTemplate showAlertWithOwner:self.presentedViewController title:title message:message actionOk:^(UIAlertAction * _Nonnull okAction) {
  193. if (closeOnOk) {
  194. [self.extensionContext completeRequestReturningItems:@[] completionHandler:^(BOOL expired) {
  195. [self commonCompletionHandler];
  196. }];
  197. }
  198. }];
  199. }
  200. - (void)showAlert:(UIAlertController *)alertController {
  201. if (self.presentedViewController) {
  202. [self.presentedViewController presentViewController:alertController animated:YES completion:nil];
  203. } else {
  204. [self presentViewController:alertController animated:YES completion:nil];
  205. }
  206. }
  207. - (void)loadItemsFromContext {
  208. _itemManager = [[SenderItemManager alloc] init];
  209. _itemManager.delegate = self;
  210. for (NSExtensionItem *item in self.extensionContext.inputItems) {
  211. for (NSItemProvider *itemProvider in item.attachments) {
  212. NSString *baseUTI = [self getBaseUTIType:itemProvider];
  213. NSString *secondUTI = [self getSecondUTIType:itemProvider];
  214. [_itemManager addItem:itemProvider forType:baseUTI secondType:secondUTI];
  215. }
  216. }
  217. }
  218. - (NSString *)getBaseUTIType:(NSItemProvider *)itemProvider {
  219. NSMutableArray *typeIdentifiers = [NSMutableArray arrayWithArray:itemProvider.registeredTypeIdentifiers];
  220. if (@available(iOS 13.0, *)) {
  221. if ([typeIdentifiers count] >= 1) {
  222. return [typeIdentifiers lastObject];
  223. }
  224. return UTTYPE_FILE_URL;
  225. } else {
  226. if ([itemProvider hasItemConformingToTypeIdentifier:UTTYPE_FILE_URL]) {
  227. [typeIdentifiers removeObject:UTTYPE_FILE_URL];
  228. }
  229. // take the first type
  230. if ([typeIdentifiers count] >= 1) {
  231. return [typeIdentifiers firstObject];
  232. }
  233. return UTTYPE_FILE_URL;
  234. }
  235. }
  236. - (NSString *)getSecondUTIType:(NSItemProvider *)itemProvider {
  237. NSMutableArray *typeIdentifiers = [NSMutableArray arrayWithArray:itemProvider.registeredTypeIdentifiers];
  238. if (@available(iOS 13.0, *)) {
  239. if ([itemProvider hasItemConformingToTypeIdentifier:UTTYPE_FILE_URL]) {
  240. [typeIdentifiers removeObject:UTTYPE_FILE_URL];
  241. }
  242. if ([typeIdentifiers count] >= 1) {
  243. return [typeIdentifiers firstObject];
  244. }
  245. return UTTYPE_FILE_URL;
  246. } else {
  247. return nil;
  248. }
  249. }
  250. - (BOOL)canConnect {
  251. return [ServerConnector sharedServerConnector].connectionState == ConnectionStateLoggedIn;
  252. }
  253. - (void)showProgressUI {
  254. _progressViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"ProgressViewController"];
  255. _progressViewController.delegate = self;
  256. _progressViewController.totalCount = _itemManager.itemCount * [_recipientConversations count];
  257. self.presentedViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
  258. _progressViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
  259. if ([self.presentedViewController isKindOfClass:[ModalNavigationController class]]) {
  260. ((ModalNavigationController *)self.presentedViewController).modalDelegate = nil;
  261. }
  262. [self dismissViewControllerAnimated:YES completion:^{
  263. [self presentViewController:_progressViewController animated:YES completion:^{
  264. [self sendItems];
  265. }];
  266. }];
  267. }
  268. - (void)checkFeatureMaskAndSendItems {
  269. if ([_itemManager containsFileItem]) {
  270. [FeatureMask checkFeatureMask:FEATURE_MASK_FILE_TRANSFER forConversations:_recipientConversations onCompletion:^(NSArray *unsupportedContacts) {
  271. if ([unsupportedContacts count] > 0) {
  272. [self recipientConversationsRemoveContacts:unsupportedContacts];
  273. NSString *messageFormat;
  274. if ([_recipientConversations count] == 0) {
  275. messageFormat = [BundleUtil localizedStringForKey:@"error_message_none_feature_level"];
  276. } else {
  277. messageFormat = [BundleUtil localizedStringForKey:@"error_message_feature_level"];
  278. }
  279. NSString *participantNames = [Utils stringFromContacts:unsupportedContacts];
  280. NSString *message = [NSString stringWithFormat:messageFormat, participantNames];
  281. NSString *title = [BundleUtil localizedStringForKey:@"error_title_feature_level"];
  282. [UIAlertTemplate showAlertWithOwner:self title:title message:message actionOk:^(UIAlertAction * _Nonnull okAction) {
  283. [self startSending];
  284. }];
  285. } else {
  286. [self startSending];
  287. }
  288. }];
  289. } else {
  290. [self startSending];
  291. }
  292. }
  293. - (void)recipientConversationsRemoveContacts:(NSArray *)contacts {
  294. NSMutableSet *newRecipientConversations = [NSMutableSet setWithSet:_recipientConversations];
  295. for (Contact *contact in contacts) {
  296. for (Conversation *conversation in _recipientConversations) {
  297. if (conversation.isGroup) {
  298. if ([conversation.members isEqualToSet:[NSSet setWithArray:contacts]]) {
  299. [newRecipientConversations removeObject:conversation];
  300. }
  301. } else if (conversation.contact == contact) {
  302. [newRecipientConversations removeObject:conversation];
  303. }
  304. }
  305. }
  306. _recipientConversations = newRecipientConversations;
  307. }
  308. - (void)sendItems {
  309. [_itemManager sendItemsTo:_recipientConversations];
  310. }
  311. - (void)startSending {
  312. NSInteger count = [_recipientConversations count] * _itemManager.itemCount;
  313. if (count == 0) {
  314. if (_itemManager.itemCount == 0) {
  315. // exit only if no items to send, otherwise the user has the chance to select another recipient
  316. [self finishAndClose];
  317. }
  318. return;
  319. }
  320. if ([self canConnect] == NO) {
  321. NSString *title = NSLocalizedString(@"cannot_connect_title", nil);
  322. NSString *message = NSLocalizedString(@"cannot_connect_message", nil);
  323. [self showAlertWithTitle:title message:message closeOnOk:NO];
  324. return;
  325. }
  326. dispatch_async(dispatch_get_main_queue(), ^{
  327. [self showProgressUI];
  328. });
  329. }
  330. - (void)commonCompletionHandler {
  331. [AppGroup setActive:NO forType:AppGroupTypeShareExtension];
  332. }
  333. - (void)completionHandler:(BOOL)expired {
  334. [self commonCompletionHandler];
  335. if (expired) {
  336. _itemManager.shouldCancel = YES;
  337. [[ServerConnector sharedServerConnector] disconnect];
  338. } else {
  339. [[ServerConnector sharedServerConnector] disconnectWait];
  340. }
  341. }
  342. - (void)finishAndClose {
  343. [AppGroup setActive:NO forType:AppGroupTypeShareExtension];
  344. [[MessageQueue sharedMessageQueue] save];
  345. NSInteger delay = 0;
  346. if (_progressViewController != nil) {
  347. // show progress for long enough & give server connection enough time to handle acks
  348. delay = 1;
  349. }
  350. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  351. [self.extensionContext completeRequestReturningItems:@[] completionHandler:^(BOOL expired) {
  352. [self completionHandler:expired];
  353. }];
  354. });
  355. }
  356. #pragma mark - ContactGroupPickerDelegate
  357. - (void)contactPicker:(ContactGroupPickerViewController*)contactPicker didPickConversations:(NSSet *)conversations renderType:(NSNumber *)renderType sendAsFile:(BOOL)sendAsFile {
  358. _recipientConversations = [NSMutableSet set];
  359. _itemManager.sendAsFile = sendAsFile;
  360. if (contactPicker.additionalTextToSend) {
  361. [_itemManager addText:contactPicker.additionalTextToSend];
  362. }
  363. for (Conversation *conversation in conversations) {
  364. [_recipientConversations addObject:conversation];
  365. }
  366. [self checkFeatureMaskAndSendItems];
  367. }
  368. - (void)contactPickerDidCancel:(ContactGroupPickerViewController*)contactPicker {
  369. [self finishAndClose];
  370. }
  371. #pragma mark - Passcode lock delegate
  372. - (void)shouldEraseApplicationData:(JKLLockScreenViewController *)viewController {
  373. // do not delete stuff from within extension, just quit
  374. [self.extensionContext completeRequestReturningItems:@[] completionHandler:^(BOOL expired) {
  375. [self commonCompletionHandler];
  376. }];
  377. }
  378. - (void)didPasscodeEnteredIncorrectly:(JKLLockScreenViewController *)viewController {
  379. if (_passcodeTryCount >= MAX_NUM_PASSCODE_TRIES) {
  380. [self.extensionContext completeRequestReturningItems:@[] completionHandler:^(BOOL expired) {
  381. [self commonCompletionHandler];
  382. }];
  383. }
  384. _passcodeTryCount++;
  385. }
  386. - (void)didPasscodeEnteredCorrectly:(JKLLockScreenViewController *)viewController {
  387. _isAuthorized = YES;
  388. }
  389. - (void)unlockWasCancelledLockScreenViewController:(JKLLockScreenViewController *)lockScreenViewController {
  390. [self finishAndClose];
  391. }
  392. - (void)didPasscodeViewDismiss:(JKLLockScreenViewController *)viewController {
  393. if (_isAuthorized) {
  394. [self presentSharingUI];
  395. }
  396. }
  397. #pragma mark - ProgressViewDelegate
  398. - (void)progressViewDidCancel {
  399. _itemManager.shouldCancel = YES;
  400. dispatch_async(dispatch_get_main_queue(), ^{
  401. [self finishAndClose];
  402. });
  403. }
  404. #pragma mark - ModalNavigationControllerDelegate
  405. - (void)willDismissModalNavigationController {
  406. [self finishAndClose];
  407. }
  408. #pragma mark - SenderItemDelegate
  409. - (void)showAlertWithTitle:(NSString *)title message:(NSString *)message {
  410. dispatch_async(dispatch_get_main_queue(), ^{
  411. [self showAlertWithTitle:title message:message closeOnOk:YES];
  412. });
  413. }
  414. - (void)finishedItem:(id)item {
  415. [_progressViewController finishedItem:item];
  416. }
  417. - (void)setProgress:(NSNumber *)progress forItem:(id)item {
  418. [_progressViewController setProgress:progress forItem:item];
  419. }
  420. - (void)setFinished {
  421. [self finishAndClose];
  422. }
  423. #pragma mark UIApplicationDelegate
  424. - (void)didBecomeActive:(NSNotification*)notification {
  425. [AppGroup setActive:YES forType:AppGroupTypeShareExtension];
  426. }
  427. @end