From 08d36aa8625c765e78d9983058b8ac3600d4ce22 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 19 Apr 2018 11:45:13 -0400 Subject: [PATCH] Add screen lock UI to SAE. --- Signal.xcodeproj/project.pbxproj | 22 ++- Signal/src/util/OWSScreenLockUI.m | 154 +++------------ .../ScreenLockViewController.h | 29 +++ .../ScreenLockViewController.m | 139 +++++++++++++ .../attachments/ShareViewDelegate.swift | 3 +- .../utils}/OWSScreenLock.swift | 41 ++-- SignalServiceKit/src/Util/AppContext.h | 2 + SignalServiceKit/src/Util/AppContext.m | 12 ++ .../SAEScreenLockViewController.h | 18 ++ .../SAEScreenLockViewController.m | 185 ++++++++++++++++++ .../ShareViewController.swift | 70 ++++++- .../SignalShareExtension-Bridging-Header.h | 1 + 12 files changed, 520 insertions(+), 156 deletions(-) create mode 100644 SignalMessaging/ViewControllers/ScreenLockViewController.h create mode 100644 SignalMessaging/ViewControllers/ScreenLockViewController.m rename {Signal/src/util => SignalMessaging/utils}/OWSScreenLock.swift (92%) create mode 100644 SignalShareExtension/SAEScreenLockViewController.h create mode 100644 SignalShareExtension/SAEScreenLockViewController.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 31773b5e8..8e4efe097 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -142,6 +142,10 @@ 34612A011FD5F31400532771 /* OWS104CreateRecipientIdentities.h in Headers */ = {isa = PBXBuildFile; fileRef = 346129F41FD5F31400532771 /* OWS104CreateRecipientIdentities.h */; }; 34612A061FD7238600532771 /* OWSContactsSyncing.h in Headers */ = {isa = PBXBuildFile; fileRef = 34612A041FD7238500532771 /* OWSContactsSyncing.h */; settings = {ATTRIBUTES = (Public, ); }; }; 34612A071FD7238600532771 /* OWSContactsSyncing.m in Sources */ = {isa = PBXBuildFile; fileRef = 34612A051FD7238500532771 /* OWSContactsSyncing.m */; }; + 34641E182088D7E900E2EDE5 /* OWSScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34641E172088D7E900E2EDE5 /* OWSScreenLock.swift */; }; + 34641E1B2088DA4100E2EDE5 /* ScreenLockViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34641E192088DA3F00E2EDE5 /* ScreenLockViewController.m */; }; + 34641E1C2088DA4100E2EDE5 /* ScreenLockViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 34641E1A2088DA4000E2EDE5 /* ScreenLockViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34641E1F2088DA6D00E2EDE5 /* SAEScreenLockViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34641E1E2088DA6D00E2EDE5 /* SAEScreenLockViewController.m */; }; 346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */; }; 347850311FD7494A007B8332 /* dripicons-v2.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5B1E787A9800DF2FB9 /* dripicons-v2.ttf */; }; 347850321FD7494A007B8332 /* ElegantIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */; }; @@ -206,7 +210,6 @@ 34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */; }; 34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */; }; 34D2CCD220618B3000CB1A14 /* OWSBackupLazyRestoreJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD120618B2F00CB1A14 /* OWSBackupLazyRestoreJob.swift */; }; - 34D2CCD4206294B900CB1A14 /* OWSScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD3206294B900CB1A14 /* OWSScreenLock.swift */; }; 34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */; }; 34D2CCDF206939B400CB1A14 /* DebugUIMessagesAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCDB206939B100CB1A14 /* DebugUIMessagesAction.m */; }; 34D2CCE0206939B400CB1A14 /* DebugUIMessagesAssetLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCDC206939B200CB1A14 /* DebugUIMessagesAssetLoader.m */; }; @@ -726,6 +729,11 @@ 346129F41FD5F31400532771 /* OWS104CreateRecipientIdentities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWS104CreateRecipientIdentities.h; sourceTree = ""; }; 34612A041FD7238500532771 /* OWSContactsSyncing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactsSyncing.h; sourceTree = ""; }; 34612A051FD7238500532771 /* OWSContactsSyncing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsSyncing.m; sourceTree = ""; }; + 34641E172088D7E900E2EDE5 /* OWSScreenLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSScreenLock.swift; sourceTree = ""; }; + 34641E192088DA3F00E2EDE5 /* ScreenLockViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ScreenLockViewController.m; path = SignalMessaging/ViewControllers/ScreenLockViewController.m; sourceTree = SOURCE_ROOT; }; + 34641E1A2088DA4000E2EDE5 /* ScreenLockViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ScreenLockViewController.h; path = SignalMessaging/ViewControllers/ScreenLockViewController.h; sourceTree = SOURCE_ROOT; }; + 34641E1D2088DA6C00E2EDE5 /* SAEScreenLockViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SAEScreenLockViewController.h; sourceTree = ""; }; + 34641E1E2088DA6D00E2EDE5 /* SAEScreenLockViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SAEScreenLockViewController.m; sourceTree = ""; }; 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropScaleImageViewController.swift; sourceTree = ""; }; 347850561FD86544007B8332 /* SAEFailedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEFailedViewController.swift; sourceTree = ""; }; 347850581FD9972E007B8332 /* SwiftSingletons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftSingletons.swift; sourceTree = ""; }; @@ -828,7 +836,6 @@ 34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AttachmentUploadView.m; sourceTree = ""; }; 34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRecipientStatusUtils.swift; sourceTree = ""; }; 34D2CCD120618B2F00CB1A14 /* OWSBackupLazyRestoreJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSBackupLazyRestoreJob.swift; sourceTree = ""; }; - 34D2CCD3206294B900CB1A14 /* OWSScreenLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSScreenLock.swift; sourceTree = ""; }; 34D2CCD82062E7D000CB1A14 /* OWSScreenLockUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSScreenLockUI.h; sourceTree = ""; }; 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSScreenLockUI.m; sourceTree = ""; }; 34D2CCDB206939B100CB1A14 /* DebugUIMessagesAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIMessagesAction.m; sourceTree = ""; }; @@ -1408,6 +1415,7 @@ 344F248E2007D7F200CFB4F4 /* OWSMessagesBubbleImageFactory.swift */, 346129371FD1B47200532771 /* OWSPreferences.h */, 346129381FD1B47200532771 /* OWSPreferences.m */, + 34641E172088D7E900E2EDE5 /* OWSScreenLock.swift */, 34480B4F1FD0A7A300BC14EF /* OWSScrubbingLogFormatter.h */, 34480B511FD0A7A400BC14EF /* OWSScrubbingLogFormatter.m */, 346129331FD1A88700532771 /* OWSSwiftUtils.swift */, @@ -1577,6 +1585,8 @@ 344F248620069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift */, 344D6CE920069E070042AF96 /* NewNonContactConversationViewController.h */, 344D6CE820069E070042AF96 /* NewNonContactConversationViewController.m */, + 34641E1A2088DA4000E2EDE5 /* ScreenLockViewController.h */, + 34641E192088DA3F00E2EDE5 /* ScreenLockViewController.m */, 344D6CE620069E060042AF96 /* SelectRecipientViewController.h */, 344D6CE720069E060042AF96 /* SelectRecipientViewController.m */, 344F2495200FD03200CFB4F4 /* SharingThreadPickerViewController.h */, @@ -1812,6 +1822,8 @@ 4535186C1FC635DD00210559 /* MainInterface.storyboard */, 347850561FD86544007B8332 /* SAEFailedViewController.swift */, 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */, + 34641E1D2088DA6C00E2EDE5 /* SAEScreenLockViewController.h */, + 34641E1E2088DA6D00E2EDE5 /* SAEScreenLockViewController.m */, 4535186A1FC635DD00210559 /* ShareViewController.swift */, 34480B371FD092A900BC14EF /* SignalShareExtension-Bridging-Header.h */, 34480B381FD092E300BC14EF /* SignalShareExtension-Prefix.pch */, @@ -2030,7 +2042,6 @@ 340FC8CE205BF2FA007AEB0F /* OWSBackupIO.m */, 340FC8CB20518C76007AEB0F /* OWSBackupJob.h */, 340FC8CC20518C76007AEB0F /* OWSBackupJob.m */, - 34D2CCD3206294B900CB1A14 /* OWSScreenLock.swift */, 34D2CCD82062E7D000CB1A14 /* OWSScreenLockUI.h */, 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */, 34D2CCD120618B2F00CB1A14 /* OWSBackupLazyRestoreJob.swift */, @@ -2328,6 +2339,7 @@ files = ( 451F8A3A1FD711D9005CB9DA /* ContactsViewHelper.h in Headers */, 34480B491FD0A60200BC14EF /* OWSMath.h in Headers */, + 34641E1C2088DA4100E2EDE5 /* ScreenLockViewController.h in Headers */, 346129E71FD5C0C600532771 /* OWSDatabaseMigrationRunner.h in Headers */, 344D6CEA20069E070042AF96 /* SelectRecipientViewController.h in Headers */, 34480B521FD0A7A400BC14EF /* OWSLogger.h in Headers */, @@ -3067,6 +3079,7 @@ files = ( 4535186B1FC635DD00210559 /* ShareViewController.swift in Sources */, 34480B361FD0929200BC14EF /* ShareAppExtensionContext.m in Sources */, + 34641E1F2088DA6D00E2EDE5 /* SAEScreenLockViewController.m in Sources */, 3461284B1FD0B94000532771 /* SAELoadViewController.swift in Sources */, 347850571FD86544007B8332 /* SAEFailedViewController.swift in Sources */, ); @@ -3098,6 +3111,7 @@ 34480B641FD0A98800BC14EF /* UIView+OWS.m in Sources */, 34C3C7932040B0DD0000134C /* OWSAudioPlayer.m in Sources */, 3461293A1FD1B47300532771 /* OWSPreferences.m in Sources */, + 34641E1B2088DA4100E2EDE5 /* ScreenLockViewController.m in Sources */, 344F248520069E9C00CFB4F4 /* CountryCodeViewController.m in Sources */, 34480B671FD0AA9400BC14EF /* UIFont+OWS.m in Sources */, 346129E61FD5C0C600532771 /* OWSDatabaseMigrationRunner.m in Sources */, @@ -3155,6 +3169,7 @@ 344D6CEB20069E070042AF96 /* SelectRecipientViewController.m in Sources */, 34480B591FD0A7A400BC14EF /* OWSScrubbingLogFormatter.m in Sources */, 451F8A441FD7156B005CB9DA /* BlockListUIUtils.m in Sources */, + 34641E182088D7E900E2EDE5 /* OWSScreenLock.swift in Sources */, 451F8A381FD7117E005CB9DA /* OWSViewController.m in Sources */, 346129721FD1D74C00532771 /* SignalKeyingStorage.m in Sources */, 34480B561FD0A7A400BC14EF /* DebugLogger.m in Sources */, @@ -3320,7 +3335,6 @@ 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */, 34D2CCDF206939B400CB1A14 /* DebugUIMessagesAction.m in Sources */, 340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */, - 34D2CCD4206294B900CB1A14 /* OWSScreenLock.swift in Sources */, 340FC8C5204DE223007AEB0F /* DebugUIBackup.m in Sources */, 340FC8AE204DAC8D007AEB0F /* OWSSoundSettingsViewController.m in Sources */, 4579431E1E7C8CE9008ED0C0 /* Pastelog.m in Sources */, diff --git a/Signal/src/util/OWSScreenLockUI.m b/Signal/src/util/OWSScreenLockUI.m index 88c74063b..b5123d364 100644 --- a/Signal/src/util/OWSScreenLockUI.m +++ b/Signal/src/util/OWSScreenLockUI.m @@ -4,42 +4,17 @@ #import "OWSScreenLockUI.h" #import "Signal-Swift.h" +#import #import #import NS_ASSUME_NONNULL_BEGIN -typedef NS_ENUM(NSUInteger, ScreenLockUIState) { - ScreenLockUIStateNone, - // Shown while app is inactive or background, if enabled. - ScreenLockUIStateScreenProtection, - // Shown while app is active, if enabled. - ScreenLockUIStateScreenLock, -}; - -NSString *NSStringForScreenLockUIState(ScreenLockUIState value); -NSString *NSStringForScreenLockUIState(ScreenLockUIState value) -{ - switch (value) { - case ScreenLockUIStateNone: - return @"ScreenLockUIStateNone"; - case ScreenLockUIStateScreenProtection: - return @"ScreenLockUIStateScreenProtection"; - case ScreenLockUIStateScreenLock: - return @"ScreenLockUIStateScreenLock"; - } -} - const UIWindowLevel UIWindowLevel_Background = -1.f; -@interface OWSScreenLockUI () +@interface OWSScreenLockUI () @property (nonatomic) UIWindow *screenBlockingWindow; -@property (nonatomic) UIViewController *screenBlockingViewController; -@property (nonatomic) UIView *screenBlockingImageView; -@property (nonatomic) UIView *screenBlockingButton; -@property (nonatomic) NSArray *screenBlockingConstraints; -@property (nonatomic) NSString *screenBlockingSignature; // Unlike UIApplication.applicationState, this state reflects the // notifications, i.e. "did become active", "will resign active", @@ -54,8 +29,10 @@ const UIWindowLevel UIWindowLevel_Background = -1.f; // inactive in order for it to be reflected in the app switcher. @property (nonatomic) BOOL appIsInactiveOrBackground; @property (nonatomic) BOOL appIsInBackground; +@property (nonatomic) ScreenLockViewController *screenBlockingViewController; @property (nonatomic) BOOL isShowingScreenLockUI; + @property (nonatomic) BOOL didLastUnlockAttemptFail; // We want to remain in "screen lock" mode while "local auth" @@ -392,7 +369,7 @@ const UIWindowLevel UIWindowLevel_Background = -1.f; message:message buttonTitle:nil buttonAction:^(UIAlertAction *action) { - // After the alert, re-show the unlock UI. + // After the alert, update the UI. [self ensureUI]; } fromViewController:self.screenBlockingViewController]; @@ -413,52 +390,12 @@ const UIWindowLevel UIWindowLevel_Background = -1.f; window.opaque = YES; window.backgroundColor = UIColor.ows_materialBlueColor; - UIViewController *viewController = [UIViewController new]; - viewController.view.backgroundColor = UIColor.ows_materialBlueColor; - - UIView *rootView = viewController.view; - - UIView *edgesView = [UIView containerView]; - [rootView addSubview:edgesView]; - [edgesView autoPinEdgeToSuperviewEdge:ALEdgeTop]; - [edgesView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; - [edgesView autoPinWidthToSuperview]; - - UIImage *image = [UIImage imageNamed:@"logoSignal"]; - UIImageView *imageView = [UIImageView new]; - imageView.image = image; - [edgesView addSubview:imageView]; - [imageView autoHCenterInSuperview]; - - const CGSize screenSize = UIScreen.mainScreen.bounds.size; - const CGFloat shortScreenDimension = MIN(screenSize.width, screenSize.height); - const CGFloat imageSize = round(shortScreenDimension / 3.f); - [imageView autoSetDimension:ALDimensionWidth toSize:imageSize]; - [imageView autoSetDimension:ALDimensionHeight toSize:imageSize]; - - const CGFloat kButtonHeight = 40.f; - OWSFlatButton *button = - [OWSFlatButton buttonWithTitle:NSLocalizedString(@"SCREEN_LOCK_UNLOCK_SIGNAL", - @"Label for button on lock screen that lets users unlock Signal.") - font:[OWSFlatButton fontForHeight:kButtonHeight] - titleColor:[UIColor ows_materialBlueColor] - backgroundColor:[UIColor whiteColor] - target:self - selector:@selector(showUnlockUI)]; - [edgesView addSubview:button]; - - [button autoSetDimension:ALDimensionHeight toSize:kButtonHeight]; - [button autoPinLeadingToSuperviewMarginWithInset:50.f]; - [button autoPinTrailingToSuperviewMarginWithInset:50.f]; - const CGFloat kVMargin = 65.f; - [button autoPinBottomToSuperviewMarginWithInset:kVMargin]; - + ScreenLockViewController *viewController = [ScreenLockViewController new]; + viewController.delegate = self; window.rootViewController = viewController; self.screenBlockingWindow = window; self.screenBlockingViewController = viewController; - self.screenBlockingImageView = imageView; - self.screenBlockingButton = button; // Default to screen protection until we know otherwise. [self updateScreenBlockingWindow:ScreenLockUIStateNone animated:NO]; @@ -530,61 +467,9 @@ const UIWindowLevel UIWindowLevel_Background = -1.f; self.rootFrontmostViewController = nil; } - self.screenBlockingImageView.hidden = !shouldShowBlockWindow; - - UIView *rootView = self.screenBlockingViewController.view; - - BOOL shouldHaveScreenLock = desiredUIState == ScreenLockUIStateScreenLock; - NSString *signature = [NSString stringWithFormat:@"%d %d", shouldHaveScreenLock, self.isShowingScreenLockUI]; - if ([NSObject isNullableObject:self.screenBlockingSignature equalTo:signature]) { - // Skip redundant work to avoid interfering with ongoing animations. - return; - } - - [NSLayoutConstraint deactivateConstraints:self.screenBlockingConstraints]; - - NSMutableArray *screenBlockingConstraints = [NSMutableArray new]; - - self.screenBlockingButton.hidden = !shouldHaveScreenLock; - - if (self.isShowingScreenLockUI) { - const CGFloat kVMargin = 60.f; - [screenBlockingConstraints addObject:[self.screenBlockingImageView autoPinEdge:ALEdgeTop - toEdge:ALEdgeTop - ofView:rootView - withOffset:kVMargin]]; - } else { - [screenBlockingConstraints addObject:[self.screenBlockingImageView autoVCenterInSuperview]]; - } - - self.screenBlockingConstraints = screenBlockingConstraints; - self.screenBlockingSignature = signature; - - if (animated) { - [UIView animateWithDuration:0.35f - animations:^{ - [rootView layoutIfNeeded]; - }]; - } else { - [rootView layoutIfNeeded]; - } -} - -- (void)showUnlockUI -{ - OWSAssertIsOnMainThread(); - - if (self.appIsInactiveOrBackground) { - // This button can be pressed while the app is inactive - // for a brief window while the iOS auth UI is dismissing. - return; - } - - DDLogInfo(@"%@ unlockButtonTapped", self.logTag); - - self.didLastUnlockAttemptFail = NO; - - [self ensureUI]; + [self.screenBlockingViewController updateUIWithState:desiredUIState + isLogoAtTop:self.isShowingScreenLockUI + animated:animated]; } #pragma mark - Events @@ -654,6 +539,25 @@ const UIWindowLevel UIWindowLevel_Background = -1.f; [self ensureUI]; } +#pragma mark - ScreenLockViewDelegate + +- (void)unlockButtonWasTapped +{ + OWSAssertIsOnMainThread(); + + if (self.appIsInactiveOrBackground) { + // This button can be pressed while the app is inactive + // for a brief window while the iOS auth UI is dismissing. + return; + } + + DDLogInfo(@"%@ unlockButtonWasTapped", self.logTag); + + self.didLastUnlockAttemptFail = NO; + + [self ensureUI]; +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/ViewControllers/ScreenLockViewController.h b/SignalMessaging/ViewControllers/ScreenLockViewController.h new file mode 100644 index 000000000..03e9cb856 --- /dev/null +++ b/SignalMessaging/ViewControllers/ScreenLockViewController.h @@ -0,0 +1,29 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +typedef NS_ENUM(NSUInteger, ScreenLockUIState) { + ScreenLockUIStateNone, + // Shown while app is inactive or background, if enabled. + ScreenLockUIStateScreenProtection, + // Shown while app is active, if enabled. + ScreenLockUIStateScreenLock, +}; + +NSString *NSStringForScreenLockUIState(ScreenLockUIState value); + +@protocol ScreenLockViewDelegate + +- (void)unlockButtonWasTapped; + +@end + +#pragma mark - + +@interface ScreenLockViewController : UIViewController + +@property (nonatomic, weak) id delegate; + +- (void)updateUIWithState:(ScreenLockUIState)uiState isLogoAtTop:(BOOL)isLogoAtTop animated:(BOOL)animated; + +@end diff --git a/SignalMessaging/ViewControllers/ScreenLockViewController.m b/SignalMessaging/ViewControllers/ScreenLockViewController.m new file mode 100644 index 000000000..772f21df7 --- /dev/null +++ b/SignalMessaging/ViewControllers/ScreenLockViewController.m @@ -0,0 +1,139 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "ScreenLockViewController.h" +#import "UIColor+OWS.h" +#import "UIFont+OWS.h" +#import "UIView+OWS.h" +#import + +NSString *NSStringForScreenLockUIState(ScreenLockUIState value) +{ + switch (value) { + case ScreenLockUIStateNone: + return @"ScreenLockUIStateNone"; + case ScreenLockUIStateScreenProtection: + return @"ScreenLockUIStateScreenProtection"; + case ScreenLockUIStateScreenLock: + return @"ScreenLockUIStateScreenLock"; + } +} + +@interface ScreenLockViewController () + +@property (nonatomic) UIView *screenBlockingImageView; +@property (nonatomic) UIView *screenBlockingButton; +@property (nonatomic) NSArray *screenBlockingConstraints; +@property (nonatomic) NSString *screenBlockingSignature; + +@end + +#pragma mark - + +@implementation ScreenLockViewController + +- (void)loadView +{ + [super loadView]; + + self.view.backgroundColor = UIColor.ows_materialBlueColor; + + UIView *edgesView = [UIView containerView]; + [self.view addSubview:edgesView]; + [edgesView autoPinEdgeToSuperviewEdge:ALEdgeTop]; + [edgesView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + [edgesView autoPinWidthToSuperview]; + + UIImage *image = [UIImage imageNamed:@"logoSignal"]; + UIImageView *imageView = [UIImageView new]; + imageView.image = image; + [edgesView addSubview:imageView]; + [imageView autoHCenterInSuperview]; + + const CGSize screenSize = UIScreen.mainScreen.bounds.size; + const CGFloat shortScreenDimension = MIN(screenSize.width, screenSize.height); + const CGFloat imageSize = round(shortScreenDimension / 3.f); + [imageView autoSetDimension:ALDimensionWidth toSize:imageSize]; + [imageView autoSetDimension:ALDimensionHeight toSize:imageSize]; + + const CGFloat kButtonHeight = 40.f; + OWSFlatButton *button = + [OWSFlatButton buttonWithTitle:NSLocalizedString(@"SCREEN_LOCK_UNLOCK_SIGNAL", + @"Label for button on lock screen that lets users unlock Signal.") + font:[OWSFlatButton fontForHeight:kButtonHeight] + titleColor:[UIColor ows_materialBlueColor] + backgroundColor:[UIColor whiteColor] + target:self + selector:@selector(showUnlockUI)]; + [edgesView addSubview:button]; + + [button autoSetDimension:ALDimensionHeight toSize:kButtonHeight]; + [button autoPinLeadingToSuperviewMarginWithInset:50.f]; + [button autoPinTrailingToSuperviewMarginWithInset:50.f]; + const CGFloat kVMargin = 65.f; + [button autoPinBottomToSuperviewMarginWithInset:kVMargin]; + + self.screenBlockingImageView = imageView; + self.screenBlockingButton = button; +} + +// The "screen blocking" window has three possible states: +// +// * "Just a logo". Used when app is launching and in app switcher. Must match the "Launch Screen" +// storyboard pixel-for-pixel. +// * "Screen Lock, local auth UI presented". Move the Signal logo so that it is visible. +// * "Screen Lock, local auth UI not presented". Move the Signal logo so that it is visible, +// show "unlock" button. +- (void)updateUIWithState:(ScreenLockUIState)uiState isLogoAtTop:(BOOL)isLogoAtTop animated:(BOOL)animated +{ + OWSAssertIsOnMainThread(); + + BOOL shouldShowBlockWindow = uiState != ScreenLockUIStateNone; + BOOL shouldHaveScreenLock = uiState == ScreenLockUIStateScreenLock; + + self.screenBlockingImageView.hidden = !shouldShowBlockWindow; + + NSString *signature = [NSString stringWithFormat:@"%d %d", shouldHaveScreenLock, isLogoAtTop]; + if ([NSObject isNullableObject:self.screenBlockingSignature equalTo:signature]) { + // Skip redundant work to avoid interfering with ongoing animations. + return; + } + + [NSLayoutConstraint deactivateConstraints:self.screenBlockingConstraints]; + + NSMutableArray *screenBlockingConstraints = [NSMutableArray new]; + + self.screenBlockingButton.hidden = !shouldHaveScreenLock; + + if (isLogoAtTop) { + const CGFloat kVMargin = 60.f; + [screenBlockingConstraints addObject:[self.screenBlockingImageView autoPinEdge:ALEdgeTop + toEdge:ALEdgeTop + ofView:self.view + withOffset:kVMargin]]; + } else { + [screenBlockingConstraints addObject:[self.screenBlockingImageView autoVCenterInSuperview]]; + } + + self.screenBlockingConstraints = screenBlockingConstraints; + self.screenBlockingSignature = signature; + + if (animated) { + [UIView animateWithDuration:0.35f + animations:^{ + [self.view layoutIfNeeded]; + }]; + } else { + [self.view layoutIfNeeded]; + } +} + +- (void)showUnlockUI +{ + OWSAssertIsOnMainThread(); + + [self.delegate unlockButtonWasTapped]; +} + +@end diff --git a/SignalMessaging/attachments/ShareViewDelegate.swift b/SignalMessaging/attachments/ShareViewDelegate.swift index 084597152..6be9361d8 100644 --- a/SignalMessaging/attachments/ShareViewDelegate.swift +++ b/SignalMessaging/attachments/ShareViewDelegate.swift @@ -1,11 +1,12 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // import Foundation // All Observer methods will be invoked from the main thread. @objc public protocol ShareViewDelegate: class { + func shareViewWasUnlocked() func shareViewWasCompleted() func shareViewWasCancelled() func shareViewFailed(error: Error) diff --git a/Signal/src/util/OWSScreenLock.swift b/SignalMessaging/utils/OWSScreenLock.swift similarity index 92% rename from Signal/src/util/OWSScreenLock.swift rename to SignalMessaging/utils/OWSScreenLock.swift index aa911ac4b..927e1a64f 100644 --- a/Signal/src/util/OWSScreenLock.swift +++ b/SignalMessaging/utils/OWSScreenLock.swift @@ -95,15 +95,19 @@ import LocalAuthentication // MARK: - Methods + // This method should only be called: + // + // * On the main thread. + // + // Exactly one of these completions will be performed: + // + // * Asynchronously. + // * On the main thread. @objc public func tryToUnlockScreenLock(success: @escaping (() -> Void), failure: @escaping ((Error) -> Void), unexpectedFailure: @escaping ((Error) -> Void), cancel: @escaping (() -> Void)) { - guard CurrentAppContext().isMainAppAndActive else { - owsFail("\(self.logTag) \(#function) Unexpected request for 'screen lock' unlock UI while app is inactive.") - cancel() - return - } + SwiftAssertIsOnMainThread(#function) tryToVerifyLocalAuthentication(localizedReason: NSLocalizedString("SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK", comment: "Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'."), @@ -112,41 +116,44 @@ import LocalAuthentication switch outcome { case .failure(let error): + Logger.error("\(self.logTag) local authentication failed with error: \(error)") failure(self.authenticationError(errorDescription: error)) case .unexpectedFailure(let error): + Logger.error("\(self.logTag) local authentication failed with unexpected error: \(error)") unexpectedFailure(self.authenticationError(errorDescription: error)) case .success: + Logger.verbose("\(self.logTag) local authentication succeeded.") success() case .cancel: + Logger.verbose("\(self.logTag) local authentication cancelled.") cancel() } }) } - // On failure, completion is called with an error argument. - // On success or cancel, completion is called with nil argument. - // Success and cancel can be differentiated by consulting - // isScreenLockEnabled. + // This method should only be called: + // + // * On the main thread. + // + // completionParam will be performed: + // + // * Asynchronously. + // * On the main thread. private func tryToVerifyLocalAuthentication(localizedReason: String, completion completionParam: @escaping ((OWSScreenLockOutcome) -> Void)) { SwiftAssertIsOnMainThread(#function) + let defaultErrorDescription = NSLocalizedString("SCREEN_LOCK_ENABLE_UNKNOWN_ERROR", + comment: "Indicates that an unknown error occurred while using Touch ID/Face ID/Phone Passcode.") + // Ensure completion is always called on the main thread. let completion = { (outcome: OWSScreenLockOutcome) in - switch outcome { - case .failure(let error): - Logger.error("\(self.logTag) local authentication failed with error: \(error)") - default: - break - } DispatchQueue.main.async { completionParam(outcome) } } let context = screenLockContext() - let defaultErrorDescription = NSLocalizedString("SCREEN_LOCK_ENABLE_UNKNOWN_ERROR", - comment: "Indicates that an unknown error occurred while using Touch ID/Face ID/Phone Passcode.") var authError: NSError? let canEvaluatePolicy = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &authError) diff --git a/SignalServiceKit/src/Util/AppContext.h b/SignalServiceKit/src/Util/AppContext.h index 21c0ed8a8..78c77df50 100755 --- a/SignalServiceKit/src/Util/AppContext.h +++ b/SignalServiceKit/src/Util/AppContext.h @@ -17,6 +17,8 @@ extern NSString *const OWSApplicationDidBecomeActiveNotification; typedef void (^BackgroundTaskExpirationHandler)(void); +NSString *NSStringForUIApplicationState(UIApplicationState value); + @class OWSAES256Key; @protocol AppContext diff --git a/SignalServiceKit/src/Util/AppContext.m b/SignalServiceKit/src/Util/AppContext.m index 0fb572546..aa1ad7a03 100755 --- a/SignalServiceKit/src/Util/AppContext.m +++ b/SignalServiceKit/src/Util/AppContext.m @@ -11,6 +11,18 @@ NSString *const OWSApplicationWillEnterForegroundNotification = @"OWSApplication NSString *const OWSApplicationWillResignActiveNotification = @"OWSApplicationWillResignActiveNotification"; NSString *const OWSApplicationDidBecomeActiveNotification = @"OWSApplicationDidBecomeActiveNotification"; +NSString *NSStringForUIApplicationState(UIApplicationState value) +{ + switch (value) { + case UIApplicationStateActive: + return @"UIApplicationStateActive"; + case UIApplicationStateInactive: + return @"UIApplicationStateInactive"; + case UIApplicationStateBackground: + return @"UIApplicationStateBackground"; + } +} + static id currentAppContext = nil; id CurrentAppContext(void) diff --git a/SignalShareExtension/SAEScreenLockViewController.h b/SignalShareExtension/SAEScreenLockViewController.h new file mode 100644 index 000000000..737e3e993 --- /dev/null +++ b/SignalShareExtension/SAEScreenLockViewController.h @@ -0,0 +1,18 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSViewController.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ShareViewDelegate; + +@interface SAEScreenLockViewController : ScreenLockViewController + +- (instancetype)initWithShareViewDelegate:(id)shareViewDelegate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalShareExtension/SAEScreenLockViewController.m b/SignalShareExtension/SAEScreenLockViewController.m new file mode 100644 index 000000000..a446e2a30 --- /dev/null +++ b/SignalShareExtension/SAEScreenLockViewController.m @@ -0,0 +1,185 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "SAEScreenLockViewController.h" +#import "UIColor+OWS.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SAEScreenLockViewController () + +@property (nonatomic, readonly, weak) id shareViewDelegate; + +@property (nonatomic) BOOL hasShownAuthUIOnce; + +@property (nonatomic) BOOL isShowingAuthUI; + +@end + +#pragma mark - + +@implementation SAEScreenLockViewController + +- (instancetype)initWithShareViewDelegate:(id)shareViewDelegate +{ + self = [super init]; + if (!self) { + return self; + } + + _shareViewDelegate = shareViewDelegate; + + self.delegate = self; + + return self; +} + +- (void)loadView +{ + [super loadView]; + + self.view.backgroundColor = [UIColor ows_materialBlueColor]; + + self.title = NSLocalizedString(@"SHARE_EXTENSION_VIEW_TITLE", @"Title for the 'share extension' view."); + + self.navigationItem.leftBarButtonItem = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop + target:self + action:@selector(dismissPressed:)]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + [self ensureUI]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + [self ensureUI]; + + // Auto-show the auth UI f + if (!self.hasShownAuthUIOnce) { + self.hasShownAuthUIOnce = YES; + + [self tryToPresentAuthUIToUnlockScreenLock]; + } +} + +- (void)dealloc +{ + // Surface memory leaks by logging the deallocation of view controllers. + DDLogVerbose(@"Dealloc: %@", self.class); +} + +- (void)tryToPresentAuthUIToUnlockScreenLock +{ + OWSAssertIsOnMainThread(); + + if (self.isShowingAuthUI) { + // We're already showing the auth UI; abort. + return; + } + DDLogInfo(@"%@, try to unlock screen lock", self.logTag); + + self.isShowingAuthUI = YES; + + [OWSScreenLock.sharedManager tryToUnlockScreenLockWithSuccess:^{ + OWSAssertIsOnMainThread(); + + DDLogInfo(@"%@ unlock screen lock succeeded.", self.logTag); + + self.isShowingAuthUI = NO; + + [self.shareViewDelegate shareViewWasUnlocked]; + } + failure:^(NSError *error) { + OWSAssertIsOnMainThread(); + + DDLogInfo(@"%@ unlock screen lock failed.", self.logTag); + + self.isShowingAuthUI = NO; + + [self ensureUI]; + + [self showScreenLockFailureAlertWithMessage:error.localizedDescription]; + } + unexpectedFailure:^(NSError *error) { + OWSAssertIsOnMainThread(); + + DDLogInfo(@"%@ unlock screen lock unexpectedly failed.", self.logTag); + + self.isShowingAuthUI = NO; + + // Local Authentication isn't working properly. + // This isn't covered by the docs or the forums but in practice + // it appears to be effective to retry again after waiting a bit. + dispatch_async(dispatch_get_main_queue(), ^{ + [self ensureUI]; + }); + } + cancel:^{ + OWSAssertIsOnMainThread(); + + DDLogInfo(@"%@ unlock screen lock cancelled.", self.logTag); + + self.isShowingAuthUI = NO; + + [self ensureUI]; + }]; + + [self ensureUI]; +} + +- (void)ensureUI +{ + [self updateUIWithState:ScreenLockUIStateScreenLock isLogoAtTop:NO animated:NO]; +} + +- (void)showScreenLockFailureAlertWithMessage:(NSString *)message +{ + OWSAssertIsOnMainThread(); + + [OWSAlerts showAlertWithTitle:NSLocalizedString(@"SCREEN_LOCK_UNLOCK_FAILED", + @"Title for alert indicating that screen lock could not be unlocked.") + message:message + buttonTitle:nil + buttonAction:^(UIAlertAction *action) { + // After the alert, update the UI. + [self ensureUI]; + } + fromViewController:self]; +} + +- (void)dismissPressed:(id)sender +{ + DDLogDebug(@"%@ tapped dismiss share button", self.logTag); + + [self cancelShareExperience]; +} + +- (void)cancelShareExperience +{ + [self.shareViewDelegate shareViewWasCancelled]; +} + +#pragma mark - ScreenLockViewDelegate + +- (void)unlockButtonWasTapped +{ + OWSAssertIsOnMainThread(); + + DDLogInfo(@"%@ unlockButtonWasTapped", self.logTag); + + [self tryToPresentAuthUIToUnlockScreenLock]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalShareExtension/ShareViewController.swift b/SignalShareExtension/ShareViewController.swift index 8fb66c3e7..c952e4a23 100644 --- a/SignalShareExtension/ShareViewController.swift +++ b/SignalShareExtension/ShareViewController.swift @@ -121,6 +121,10 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed selector: #selector(owsApplicationWillEnterForeground), name: .OWSApplicationWillEnterForeground, object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(applicationDidEnterBackground), + name: .OWSApplicationDidEnterBackground, + object: nil) Logger.info("\(self.logTag) \(#function) completed.") @@ -137,6 +141,24 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed ExitShareExtension() } + @objc + public func applicationDidEnterBackground() { + SwiftAssertIsOnMainThread(#function) + + Logger.info("\(self.logTag) \(#function)") + + if OWSScreenLock.shared.isScreenLockEnabled() { + + Logger.info("\(self.logTag) \(#function) dismissing.") + + self.dismiss(animated: false) { [weak self] in + SwiftAssertIsOnMainThread(#function) + guard let strongSelf = self else { return } + strongSelf.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + } + } + private func activate() { SwiftAssertIsOnMainThread(#function) @@ -293,6 +315,20 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed Logger.info("\(logTag) Presenting initial root view controller") + if OWSScreenLock.shared.isScreenLockEnabled() { + presentScreenLock() + } else { + presentContentView() + } + } + + private func presentContentView() { + SwiftAssertIsOnMainThread(#function) + + Logger.debug("\(self.logTag) \(#function)") + + Logger.info("\(logTag) Presenting content view") + if !TSAccountManager.isRegistered() { showNotRegisteredView() } else if !OWSProfileManager.shared().localProfileExists() { @@ -302,7 +338,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed } else { DispatchQueue.main.async { [weak self] in guard let strongSelf = self else { return } - strongSelf.presentConversationPicker() + strongSelf.buildAttachmentAndPresentConversationPicker() } } @@ -310,24 +346,24 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed } func startupLogging() { - Logger.info("iOS Version: \(UIDevice.current.systemVersion)}") + Logger.info("\(self.logTag) iOS Version: \(UIDevice.current.systemVersion)}") let locale = NSLocale.current as NSLocale if let localeIdentifier = locale.object(forKey: NSLocale.Key.identifier) as? String, localeIdentifier.count > 0 { - Logger.info("Locale Identifier: \(localeIdentifier)") + Logger.info("\(self.logTag) Locale Identifier: \(localeIdentifier)") } else { owsFail("Locale Identifier: Unknown") } if let countryCode = locale.object(forKey: NSLocale.Key.countryCode) as? String, countryCode.count > 0 { - Logger.info("Country Code: \(countryCode)") + Logger.info("\(self.logTag) Country Code: \(countryCode)") } else { owsFail("Country Code: Unknown") } if let languageCode = locale.object(forKey: NSLocale.Key.languageCode) as? String, languageCode.count > 0 { - Logger.info("Language Code: \(languageCode)") + Logger.info("\(self.logTag) Language Code: \(languageCode)") } else { owsFail("Language Code: Unknown") } @@ -436,6 +472,12 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed // MARK: ShareViewDelegate, SAEFailedViewDelegate + public func shareViewWasUnlocked() { + Logger.info("\(self.logTag) \(#function)") + + presentContentView() + } + public func shareViewWasCompleted() { Logger.info("\(self.logTag) \(#function)") @@ -488,20 +530,21 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed } } - private func presentConversationPicker() { + private func buildAttachmentAndPresentConversationPicker() { SwiftAssertIsOnMainThread(#function) self.buildAttachment().then { [weak self] attachment -> Void in SwiftAssertIsOnMainThread(#function) guard let strongSelf = self else { return } + strongSelf.progressPoller = nil + strongSelf.loadViewController = nil + let conversationPicker = SharingThreadPickerViewController(shareViewDelegate: strongSelf) Logger.debug("\(strongSelf.logTag) presentConversationPicker: \(conversationPicker)") conversationPicker.attachment = attachment - strongSelf.progressPoller = nil - strongSelf.loadViewController = nil strongSelf.showPrimaryViewController(conversationPicker) - Logger.info("showing picker with attachment: \(attachment)") + Logger.info("\(strongSelf.logTag) showing picker with attachment: \(attachment)") }.catch {[weak self] error in SwiftAssertIsOnMainThread(#function) guard let strongSelf = self else { return } @@ -517,6 +560,15 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed }.retainUntilComplete() } + private func presentScreenLock() { + SwiftAssertIsOnMainThread(#function) + + let screenLockUI = SAEScreenLockViewController(shareViewDelegate: self) + Logger.debug("\(self.logTag) presentScreenLock: \(screenLockUI)") + showPrimaryViewController(screenLockUI) + Logger.info("\(self.logTag) showing screen lock") + } + private class func itemMatchesSpecificUtiType(itemProvider: NSItemProvider, utiType: String) -> Bool { // URLs, contacts and other special items have to be detected separately. // Many shares (e.g. pdfs) will register many UTI types and/or conform to kUTTypeData. diff --git a/SignalShareExtension/SignalShareExtension-Bridging-Header.h b/SignalShareExtension/SignalShareExtension-Bridging-Header.h index ca17b26ea..b634ebde8 100644 --- a/SignalShareExtension/SignalShareExtension-Bridging-Header.h +++ b/SignalShareExtension/SignalShareExtension-Bridging-Header.h @@ -6,6 +6,7 @@ #import // Separate iOS Frameworks from other imports. +#import "SAEScreenLockViewController.h" #import "ShareAppExtensionContext.h" #import #import