Add screen lock feature.

pull/1/head
Matthew Chen 7 years ago
parent 1612642c26
commit b62736d7d4

@ -62,11 +62,19 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
@interface AppDelegate ()
@property (nonatomic) UIWindow *screenProtectionWindow;
@property (nonatomic) BOOL hasInitialRootViewController;
@property (nonatomic) BOOL areVersionMigrationsComplete;
@property (nonatomic) BOOL didAppLaunchFail;
// Unlike UIApplication.applicationState, this state is
// updated conservatively, e.g. the flag is cleared during
// "will enter background."
@property (nonatomic) BOOL appIsInactive;
@property (nonatomic, nullable) NSDate *appBecameInactiveDate;
@property (nonatomic) UIWindow *screenBlockingWindow;
@property (nonatomic) BOOL hasUnlockedScreenLock;
@property (nonatomic) BOOL isShowingScreenLockUI;
@end
#pragma mark -
@ -79,6 +87,8 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
DDLogWarn(@"%@ applicationDidEnterBackground.", self.logTag);
[DDLog flushLog];
self.appIsInactive = YES;
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
@ -207,6 +217,10 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
selector:@selector(registrationLockDidChange:)
name:NSNotificationName_2FAStateDidChange
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(screenLockDidChange:)
name:OWSScreenLock.ScreenLockDidChange
object:nil];
DDLogInfo(@"%@ application: didFinishLaunchingWithOptions completed.", self.logTag);
@ -583,8 +597,6 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
if (CurrentAppContext().isRunningTests) {
return;
}
[self removeScreenProtection];
[self ensureRootViewController];
@ -592,6 +604,8 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
[self handleActivation];
}];
self.appIsInactive = NO;
DDLogInfo(@"%@ applicationDidBecomeActive completed.", self.logTag);
}
@ -709,23 +723,15 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
DDLogWarn(@"%@ applicationWillResignActive.", self.logTag);
__block OWSBackgroundTask *backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
self.appIsInactive = YES;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
__block OWSBackgroundTask *backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
[AppReadiness runNowOrWhenAppIsReady:^{
if ([TSAccountManager isRegistered]) {
dispatch_async(dispatch_get_main_queue(), ^{
if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive) {
// If app has not re-entered active, show screen protection if necessary.
[self showScreenProtection];
}
[SignalApp.sharedApp.homeViewController updateInboxCountLabel];
backgroundTask = nil;
});
} else {
backgroundTask = nil;
[SignalApp.sharedApp.homeViewController updateInboxCountLabel];
}
});
backgroundTask = nil;
}];
[DDLog flushLog];
}
@ -923,40 +929,6 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
return NO;
}
/**
* Screen protection obscures the app screen shown in the app switcher.
*/
- (void)prepareScreenProtection
{
OWSAssertIsOnMainThread();
UIWindow *window = [[UIWindow alloc] initWithFrame:self.window.bounds];
window.hidden = YES;
window.opaque = YES;
window.userInteractionEnabled = NO;
window.windowLevel = CGFLOAT_MAX;
window.backgroundColor = UIColor.ows_materialBlueColor;
window.rootViewController =
[[UIStoryboard storyboardWithName:@"Launch Screen" bundle:nil] instantiateInitialViewController];
self.screenProtectionWindow = window;
}
- (void)showScreenProtection
{
OWSAssertIsOnMainThread();
if (Environment.preferences.screenSecurityIsEnabled) {
self.screenProtectionWindow.hidden = NO;
}
}
- (void)removeScreenProtection {
OWSAssertIsOnMainThread();
self.screenProtectionWindow.hidden = YES;
}
#pragma mark Push Notifications Delegate Methods
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
@ -1180,6 +1152,8 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
[self ensureRootViewController];
[OWSBackup.sharedManager setup];
[self ensureScreenProtection];
}
- (void)registrationStateDidChange
@ -1205,6 +1179,8 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
// For non-legacy users, read receipts are on by default.
[OWSReadReceiptManager.sharedManager setAreReadReceiptsEnabled:YES];
}
[self ensureScreenProtection];
}
- (void)registrationLockDidChange:(NSNotification *)notification
@ -1239,6 +1215,151 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
}
[AppUpdateNag.sharedInstance showAppUpgradeNagIfNecessary];
[self ensureScreenProtection];
}
#pragma mark - Screen Lock and Protection
- (void)setAppIsInactive:(BOOL)appIsInactive
{
if (appIsInactive) {
if (!_appIsInactive) {
// Whenever app becomes inactive, clear this state.
self.hasUnlockedScreenLock = NO;
// Note the time when app became inactive.
self.appBecameInactiveDate = [NSDate new];
}
}
_appIsInactive = appIsInactive;
[self ensureScreenProtection];
}
- (void)ensureScreenProtection
{
OWSAssertIsOnMainThread();
if (!AppReadiness.isAppReady) {
[AppReadiness runNowOrWhenAppIsReady:^{
[self ensureScreenProtection];
}];
return;
}
// Don't show 'Screen Protection' if:
//
// * App is active or...
// * 'Screen Protection' is not enabled.
BOOL shouldHaveScreenProtection = (self.appIsInactive && Environment.preferences.screenSecurityIsEnabled);
BOOL shouldHaveScreenLock = NO;
if (self.appIsInactive) {
// Don't show 'Screen Lock' if app is inactive.
} else if (![TSAccountManager isRegistered]) {
// Don't show 'Screen Lock' if user is not registered.
} else if (!OWSScreenLock.sharedManager.isScreenLockEnabled) {
// Don't show 'Screen Lock' if 'Screen Lock' isn't enabled.
} else if (self.hasUnlockedScreenLock) {
// Don't show 'Screen Lock' if 'Screen Lock' has been unlocked.
} else if (!self.appBecameInactiveDate) {
// Show 'Screen Lock' if app hasn't become inactive yet (just launched).
shouldHaveScreenLock = YES;
} else {
OWSAssert(self.appBecameInactiveDate);
NSTimeInterval screenLockInterval = fabs([self.appBecameInactiveDate timeIntervalSinceNow]);
NSTimeInterval screenLockTimeout = OWSScreenLock.sharedManager.screenLockTimeout;
OWSAssert(screenLockInterval >= 0);
OWSAssert(screenLockTimeout >= 0);
if (self.appBecameInactiveDate && screenLockInterval < screenLockTimeout) {
// Don't show 'Screen Lock' if 'Screen Lock' timeout hasn't elapsed.
shouldHaveScreenProtection = YES;
// Check again when screen lock timeout should elapse.
NSTimeInterval screenLockRemaining = screenLockTimeout - screenLockInterval + 0.2f;
OWSAssert(screenLockRemaining >= 0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(screenLockRemaining * NSEC_PER_SEC)),
dispatch_get_main_queue(),
^{
[self ensureScreenProtection];
});
} else {
// Otherwise, show 'Screen Lock'.
shouldHaveScreenLock = YES;
}
}
BOOL shouldShowBlockWindow = shouldHaveScreenProtection || shouldHaveScreenLock;
self.screenBlockingWindow.hidden = !shouldShowBlockWindow;
if (shouldHaveScreenLock) {
if (!self.isShowingScreenLockUI) {
self.isShowingScreenLockUI = YES;
[OWSScreenLock.sharedManager tryToUnlockScreenLockWithSuccess:^{
DDLogInfo(@"%@ unlock screen lock succeeded.", self.logTag);
self.isShowingScreenLockUI = NO;
self.hasUnlockedScreenLock = YES;
[self ensureScreenProtection];
}
failure:^(NSError *error) {
DDLogInfo(@"%@ unlock screen lock failed.", self.logTag);
self.isShowingScreenLockUI = NO;
[self showScreenLockFailureAlertWithMessage:error.localizedDescription];
}
cancel:^{
DDLogInfo(@"%@ unlock screen lock cancelled.", self.logTag);
self.isShowingScreenLockUI = NO;
[self showScreenLockFailureAlertWithMessage:
NSLocalizedString(@"SCREEN_LOCK_UNLOCK_CANCELLED",
@"Message for alert indicating that screen lock unlock was cancelled.")];
}];
}
}
}
- (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, re-show the unlock UI.
[self ensureScreenProtection];
}];
}
- (void)screenLockDidChange:(NSNotification *)notification
{
[self ensureScreenProtection];
}
// 'Screen Blocking' window obscures the app screen:
//
// * In the app switcher.
// * During 'Screen Lock' unlock process.
- (void)prepareScreenProtection
{
OWSAssertIsOnMainThread();
UIWindow *window = [[UIWindow alloc] initWithFrame:self.window.bounds];
window.hidden = YES;
window.opaque = YES;
window.userInteractionEnabled = NO;
window.windowLevel = CGFLOAT_MAX;
window.backgroundColor = UIColor.ows_materialBlueColor;
window.rootViewController =
[[UIStoryboard storyboardWithName:@"Launch Screen" bundle:nil] instantiateInitialViewController];
self.screenBlockingWindow = window;
}
@end

@ -22,6 +22,7 @@ import LocalAuthentication
private let OWSScreenLock_Collection = "OWSScreenLock_Collection"
private let OWSScreenLock_Key_IsScreenLockEnabled = "OWSScreenLock_Key_IsScreenLockEnabled"
private let OWSScreenLock_Key_ScreenLockTimeoutSeconds = "OWSScreenLock_Key_ScreenLockTimeoutSeconds"
// MARK - Singleton class
@ -37,20 +38,50 @@ import LocalAuthentication
SwiftSingletons.register(self)
}
// MARK: - Properties
@objc public func isScreenLockEnabled() -> Bool {
AssertIsOnMainThread()
if !OWSStorage.isStorageReady() {
owsFail("\(TAG) accessed screen lock state before storage is ready.")
return false
}
return self.dbConnection.bool(forKey: OWSScreenLock_Key_IsScreenLockEnabled, inCollection: OWSScreenLock_Collection, defaultValue: false)
}
private func setIsScreenLockEnabled(value: Bool) {
AssertIsOnMainThread()
assert(OWSStorage.isStorageReady())
self.dbConnection.setBool(value, forKey: OWSScreenLock_Key_IsScreenLockEnabled, inCollection: OWSScreenLock_Collection)
NotificationCenter.default.postNotificationNameAsync(OWSScreenLock.ScreenLockDidChange, object: nil)
}
@objc public func screenLockTimeout() -> TimeInterval {
AssertIsOnMainThread()
if !OWSStorage.isStorageReady() {
owsFail("\(TAG) accessed screen lock state before storage is ready.")
return 0
}
return self.dbConnection.double(forKey: OWSScreenLock_Key_ScreenLockTimeoutSeconds, inCollection: OWSScreenLock_Collection, defaultValue: 0)
}
private func setIsScreenLockEnabled(value: TimeInterval) {
AssertIsOnMainThread()
assert(OWSStorage.isStorageReady())
self.dbConnection.setDouble(value, forKey: OWSScreenLock_Key_ScreenLockTimeoutSeconds, inCollection: OWSScreenLock_Collection)
NotificationCenter.default.postNotificationNameAsync(OWSScreenLock.ScreenLockDidChange, object: nil)
}
// MARK: - Methods
// @objc public func isScreenLockSupported() -> Bool {
// AssertIsOnMainThread()
//
@ -136,6 +167,30 @@ import LocalAuthentication
})
}
@objc public func tryToUnlockScreenLock(success: @escaping (() -> Void),
failure: @escaping ((Error) -> Void),
cancel: @escaping (() -> Void)) {
tryToVerifyLocalAuthentication(defaultReason: NSLocalizedString("SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK",
comment: "Description of how and why Signal iOS uses Touch ID/Face ID to unlock 'screen lock'."),
touchIdReason: NSLocalizedString("SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK_TOUCH_ID",
comment: "Description of how and why Signal iOS uses Touch ID to unlock 'screen lock'."),
faceIdReason: NSLocalizedString("SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK_FACE_ID",
comment: "Description of how and why Signal iOS uses Face ID to unlock 'screen lock'."),
completion: { (outcome: OWSScreenLockOutcome) in
AssertIsOnMainThread()
switch outcome {
case .failure(let error):
failure(self.authenticationError(errorDescription: error))
case .success:
success()
case .cancel:
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
@ -296,16 +351,7 @@ import LocalAuthentication
return context
}
// /// Fallback button title.
// /// @discussion Allows fallback button title customization. If set to empty string, the button will be hidden.
// /// A default title "Enter Password" is used when this property is left nil.
// @property (nonatomic, nullable, copy) NSString *localizedFallbackTitle;
//
// /// Cancel button title.
// /// @discussion Allows cancel button title customization. A default title "Cancel" is used when
// /// this property is left nil or is set to empty string.
// @property (nonatomic, nullable, copy) NSString *localizedCancelTitle NS_AVAILABLE(10_12, 10_0);
//
//
// typedef NS_ENUM(NSInteger, LAAccessControlOperation)
// {

@ -1579,6 +1579,21 @@
/* Description of how and why Signal iOS uses Touch ID to enable 'screen lock'. */
"SCREEN_LOCK_REASON_ENABLE_SCREEN_LOCK_TOUCH_ID" = "Use Touch ID to lock access to Signal.";
/* Description of how and why Signal iOS uses Touch ID/Face ID to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Use Touch ID or Face ID to unlock Screen Lock.";
/* Description of how and why Signal iOS uses Face ID to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK_FACE_ID" = "Use Face ID to unlock Screen Lock.";
/* Description of how and why Signal iOS uses Touch ID to unlock 'screen lock'. */
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK_TOUCH_ID" = "Use Touch ID to unlock Screen Lock.";
/* Message for alert indicating that screen lock unlock was cancelled. */
"SCREEN_LOCK_UNLOCK_CANCELLED" = "Unlock of Screen Lock was cancelled.";
/* Title for alert indicating that screen lock could not be unlocked. */
"SCREEN_LOCK_UNLOCK_FAILED" = "Unlock of Screen Lock failed.";
/* No comment provided by engineer. */
"SEARCH_BYNAMEORNUMBER_PLACEHOLDER_TEXT" = "Search by name or number";

Loading…
Cancel
Save