mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Merge branch 'charlesmchen/screenLock'
This commit is contained in:
commit
70e95b085e
19 changed files with 934 additions and 204 deletions
|
@ -204,6 +204,8 @@
|
|||
34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */; };
|
||||
34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */; };
|
||||
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */; };
|
||||
34D2CCD4206294B900CB1A14 /* OWSScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD3206294B900CB1A14 /* OWSScreenLock.swift */; };
|
||||
34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */; };
|
||||
34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */; };
|
||||
34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0241ED3673300188D7C /* DebugUIMessages.m */; };
|
||||
34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0261ED3673300188D7C /* DebugUITableViewController.m */; };
|
||||
|
@ -809,6 +811,9 @@
|
|||
34D1F0BB1F8D108C0066283D /* AttachmentUploadView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AttachmentUploadView.h; sourceTree = "<group>"; };
|
||||
34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AttachmentUploadView.m; sourceTree = "<group>"; };
|
||||
34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRecipientStatusUtils.swift; sourceTree = "<group>"; };
|
||||
34D2CCD3206294B900CB1A14 /* OWSScreenLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSScreenLock.swift; sourceTree = "<group>"; };
|
||||
34D2CCD82062E7D000CB1A14 /* OWSScreenLockUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSScreenLockUI.h; sourceTree = "<group>"; };
|
||||
34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSScreenLockUI.m; sourceTree = "<group>"; };
|
||||
34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvatarViewHelper.h; sourceTree = "<group>"; };
|
||||
34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AvatarViewHelper.m; sourceTree = "<group>"; };
|
||||
34D8C0231ED3673300188D7C /* DebugUIMessages.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessages.h; sourceTree = "<group>"; };
|
||||
|
@ -1961,14 +1966,17 @@
|
|||
34A9105E1FFEB113000C4745 /* OWSBackup.h */,
|
||||
34A9105F1FFEB114000C4745 /* OWSBackup.m */,
|
||||
340FC8C6204DE64D007AEB0F /* OWSBackupAPI.swift */,
|
||||
340FC8CF205BF2FA007AEB0F /* OWSBackupIO.h */,
|
||||
340FC8CE205BF2FA007AEB0F /* OWSBackupIO.m */,
|
||||
340FC8BE204DB7D1007AEB0F /* OWSBackupExportJob.h */,
|
||||
340FC8BF204DB7D2007AEB0F /* OWSBackupExportJob.m */,
|
||||
340FC8C920517B84007AEB0F /* OWSBackupImportJob.h */,
|
||||
340FC8C820517B84007AEB0F /* OWSBackupImportJob.m */,
|
||||
340FC8CF205BF2FA007AEB0F /* OWSBackupIO.h */,
|
||||
340FC8CE205BF2FA007AEB0F /* OWSBackupIO.m */,
|
||||
340FC8CB20518C76007AEB0F /* OWSBackupJob.h */,
|
||||
340FC8CC20518C76007AEB0F /* OWSBackupJob.m */,
|
||||
34D2CCD3206294B900CB1A14 /* OWSScreenLock.swift */,
|
||||
34D2CCD82062E7D000CB1A14 /* OWSScreenLockUI.h */,
|
||||
34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */,
|
||||
4579431C1E7C8CE9008ED0C0 /* Pastelog.h */,
|
||||
4579431D1E7C8CE9008ED0C0 /* Pastelog.m */,
|
||||
450DF2041E0D74AC003D14BE /* Platform.swift */,
|
||||
|
@ -3209,6 +3217,7 @@
|
|||
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
|
||||
340FC8A7204DAC8D007AEB0F /* RegistrationViewController.m in Sources */,
|
||||
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,
|
||||
34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */,
|
||||
45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */,
|
||||
345BC30C2047030700257B7C /* OWS2FASettingsViewController.m in Sources */,
|
||||
340FC8CA20517B84007AEB0F /* OWSBackupImportJob.m in Sources */,
|
||||
|
@ -3236,6 +3245,7 @@
|
|||
4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */,
|
||||
34B3F87E1E8DF1700035BE1A /* InboxTableViewCell.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 */,
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#import "OWS2FASettingsViewController.h"
|
||||
#import "OWSBackup.h"
|
||||
#import "OWSNavigationController.h"
|
||||
#import "OWSScreenLockUI.h"
|
||||
#import "Pastelog.h"
|
||||
#import "PushManager.h"
|
||||
#import "RegistrationViewController.h"
|
||||
|
@ -62,7 +63,6 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
|
|||
|
||||
@interface AppDelegate ()
|
||||
|
||||
@property (nonatomic) UIWindow *screenProtectionWindow;
|
||||
@property (nonatomic) BOOL hasInitialRootViewController;
|
||||
@property (nonatomic) BOOL areVersionMigrationsComplete;
|
||||
@property (nonatomic) BOOL didAppLaunchFail;
|
||||
|
@ -190,7 +190,7 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
|
|||
[self application:application didReceiveRemoteNotification:remoteNotif];
|
||||
}
|
||||
|
||||
[self prepareScreenProtection];
|
||||
[OWSScreenLockUI.sharedManager setupWithRootWindow:self.window];
|
||||
|
||||
// Ensure OWSContactsSyncing is instantiated.
|
||||
[OWSContactsSyncing sharedManager];
|
||||
|
@ -583,8 +583,6 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
|
|||
if (CurrentAppContext().isRunningTests) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self removeScreenProtection];
|
||||
|
||||
[self ensureRootViewController];
|
||||
|
||||
|
@ -710,22 +708,12 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
|
|||
DDLogWarn(@"%@ applicationWillResignActive.", self.logTag);
|
||||
|
||||
__block OWSBackgroundTask *backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[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 +911,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 {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import PromiseKit
|
||||
|
@ -63,9 +63,9 @@ class SyncPushTokensJob: NSObject {
|
|||
}
|
||||
|
||||
Logger.warn("\(self.TAG) uploading tokens to account servers. pushToken: \(pushToken), voipToken: \(voipToken)")
|
||||
return self.accountManager.updatePushTokens(pushToken:pushToken, voipToken:voipToken).then {
|
||||
return self.accountManager.updatePushTokens(pushToken: pushToken, voipToken: voipToken).then {
|
||||
Logger.info("\(self.TAG) successfully updated push tokens on server")
|
||||
return self.recordPushTokensLocally(pushToken:pushToken, voipToken:voipToken)
|
||||
return self.recordPushTokensLocally(pushToken: pushToken, voipToken: voipToken)
|
||||
}
|
||||
}.then {
|
||||
Logger.info("\(self.TAG) completed successfully.")
|
||||
|
@ -108,9 +108,7 @@ class SyncPushTokensJob: NSObject {
|
|||
}
|
||||
|
||||
if (didTokensChange) {
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: SyncPushTokensJob.PushTokensDidChange, object: nil)
|
||||
}
|
||||
NotificationCenter.default.postNotificationNameAsync(SyncPushTokensJob.PushTokensDidChange, object: nil)
|
||||
}
|
||||
|
||||
return Promise(value: ())
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#import <SignalMessaging/Environment.h>
|
||||
#import <SignalMessaging/OWSPreferences.h>
|
||||
#import <SignalMessaging/ThreadUtil.h>
|
||||
#import <SignalServiceKit/NSString+SSK.h>
|
||||
#import <SignalServiceKit/OWS2FAManager.h>
|
||||
#import <SignalServiceKit/OWSReadReceiptManager.h>
|
||||
|
||||
|
@ -22,6 +23,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
self.title = NSLocalizedString(@"SETTINGS_PRIVACY_TITLE", @"");
|
||||
|
||||
[self observeNotifications];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
|
@ -30,6 +33,19 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)observeNotifications
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(screenLockDidChange:)
|
||||
name:OWSScreenLock.ScreenLockDidChange
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark - Table Contents
|
||||
|
||||
- (void)updateTableContents
|
||||
|
@ -38,19 +54,20 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
__weak PrivacySettingsTableViewController *weakSelf = self;
|
||||
|
||||
[contents
|
||||
addSection:[OWSTableSection
|
||||
sectionWithTitle:nil
|
||||
items:@[
|
||||
[OWSTableItem disclosureItemWithText:
|
||||
NSLocalizedString(@"SETTINGS_BLOCK_LIST_TITLE",
|
||||
@"Label for the block list section of the settings view")
|
||||
actionBlock:^{
|
||||
[weakSelf showBlocklist];
|
||||
}],
|
||||
]]];
|
||||
OWSTableSection *blocklistSection = [OWSTableSection new];
|
||||
blocklistSection.headerTitle
|
||||
= NSLocalizedString(@"SETTINGS_BLOCK_LIST_TITLE", @"Label for the block list section of the settings view");
|
||||
[blocklistSection
|
||||
addItem:[OWSTableItem disclosureItemWithText:NSLocalizedString(@"SETTINGS_BLOCK_LIST_TITLE",
|
||||
@"Label for the block list section of the settings view")
|
||||
actionBlock:^{
|
||||
[weakSelf showBlocklist];
|
||||
}]];
|
||||
[contents addSection:blocklistSection];
|
||||
|
||||
OWSTableSection *readReceiptsSection = [OWSTableSection new];
|
||||
readReceiptsSection.headerTitle
|
||||
= NSLocalizedString(@"SETTINGS_READ_RECEIPT", @"Label for the 'read receipts' setting.");
|
||||
readReceiptsSection.footerTitle = NSLocalizedString(
|
||||
@"SETTINGS_READ_RECEIPTS_SECTION_FOOTER", @"An explanation of the 'read receipts' setting.");
|
||||
[readReceiptsSection
|
||||
|
@ -61,13 +78,47 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
selector:@selector(didToggleReadReceiptsSwitch:)]];
|
||||
[contents addSection:readReceiptsSection];
|
||||
|
||||
OWSTableSection *screenLockSection = [OWSTableSection new];
|
||||
screenLockSection.headerTitle = NSLocalizedString(
|
||||
@"SETTINGS_SCREEN_LOCK_SECTION_TITLE", @"Title for the 'screen lock' section of the privacy settings.");
|
||||
screenLockSection.footerTitle = NSLocalizedString(
|
||||
@"SETTINGS_SCREEN_LOCK_SECTION_FOOTER", @"Footer for the 'screen lock' section of the privacy settings.");
|
||||
[screenLockSection
|
||||
addItem:[OWSTableItem
|
||||
switchItemWithText:NSLocalizedString(@"SETTINGS_SCREEN_LOCK_SWITCH_LABEL",
|
||||
@"Label for the 'enable screen lock' switch of the privacy settings.")
|
||||
isOn:OWSScreenLock.sharedManager.isScreenLockEnabled
|
||||
target:self
|
||||
selector:@selector(isScreenLockEnabledDidChange:)]];
|
||||
[contents addSection:screenLockSection];
|
||||
|
||||
if (OWSScreenLock.sharedManager.isScreenLockEnabled) {
|
||||
OWSTableSection *screenLockTimeoutSection = [OWSTableSection new];
|
||||
uint32_t screenLockTimeout = (uint32_t)round(OWSScreenLock.sharedManager.screenLockTimeout);
|
||||
NSString *screenLockTimeoutString = [self formatScreenLockTimeout:screenLockTimeout];
|
||||
[screenLockTimeoutSection
|
||||
addItem:[OWSTableItem
|
||||
disclosureItemWithText:
|
||||
NSLocalizedString(@"SETTINGS_SCREEN_LOCK_ACTIVITY_TIMEOUT",
|
||||
@"Label for the 'screen lock activity timeout' setting of the privacy settings.")
|
||||
detailText:screenLockTimeoutString
|
||||
actionBlock:^{
|
||||
[weakSelf showScreenLockTimeoutUI];
|
||||
}]];
|
||||
[contents addSection:screenLockTimeoutSection];
|
||||
}
|
||||
|
||||
OWSTableSection *screenSecuritySection = [OWSTableSection new];
|
||||
screenSecuritySection.headerTitle = NSLocalizedString(@"SETTINGS_SECURITY_TITLE", @"Section header");
|
||||
screenSecuritySection.footerTitle = NSLocalizedString(@"SETTINGS_SCREEN_SECURITY_DETAIL", nil);
|
||||
[screenSecuritySection addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"SETTINGS_SCREEN_SECURITY", @"")
|
||||
isOn:[Environment.preferences screenSecurityIsEnabled]
|
||||
target:weakSelf
|
||||
selector:@selector(didToggleScreenSecuritySwitch:)]];
|
||||
[screenSecuritySection
|
||||
addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"SETTINGS_SCREEN_SECURITY", @"")
|
||||
// If 'Screen Lock' is enabled, 'Screen Protection' is auto-enabled.
|
||||
isOn:([Environment.preferences screenSecurityIsEnabled]
|
||||
|| OWSScreenLock.sharedManager.isScreenLockEnabled)
|
||||
isEnabled:!OWSScreenLock.sharedManager.isScreenLockEnabled
|
||||
target:weakSelf
|
||||
selector:@selector(didToggleScreenSecuritySwitch:)]];
|
||||
[contents addSection:screenSecuritySection];
|
||||
|
||||
OWSTableSection *removeMetadataSection = [OWSTableSection new];
|
||||
|
@ -125,14 +176,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[contents addSection:callKitSection];
|
||||
}
|
||||
|
||||
OWSTableSection *historyLogsSection = [OWSTableSection new];
|
||||
historyLogsSection.headerTitle = NSLocalizedString(@"SETTINGS_HISTORYLOG_TITLE", @"Section header");
|
||||
[historyLogsSection addItem:[OWSTableItem disclosureItemWithText:NSLocalizedString(@"SETTINGS_CLEAR_HISTORY", @"")
|
||||
actionBlock:^{
|
||||
[weakSelf clearHistoryLogs];
|
||||
}]];
|
||||
[contents addSection:historyLogsSection];
|
||||
|
||||
OWSTableSection *twoFactorAuthSection = [OWSTableSection new];
|
||||
twoFactorAuthSection.headerTitle = NSLocalizedString(
|
||||
@"SETTINGS_TWO_FACTOR_AUTH_TITLE", @"Title for the 'two factor auth' section of the privacy settings.");
|
||||
|
@ -152,6 +195,14 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}]];
|
||||
[contents addSection:twoFactorAuthSection];
|
||||
|
||||
OWSTableSection *historyLogsSection = [OWSTableSection new];
|
||||
historyLogsSection.headerTitle = NSLocalizedString(@"SETTINGS_HISTORYLOG_TITLE", @"Section header");
|
||||
[historyLogsSection addItem:[OWSTableItem disclosureItemWithText:NSLocalizedString(@"SETTINGS_CLEAR_HISTORY", @"")
|
||||
actionBlock:^{
|
||||
[weakSelf clearHistoryLogs];
|
||||
}]];
|
||||
[contents addSection:historyLogsSection];
|
||||
|
||||
self.contents = contents;
|
||||
}
|
||||
|
||||
|
@ -257,6 +308,82 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[self.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
|
||||
- (void)isScreenLockEnabledDidChange:(UISwitch *)sender
|
||||
{
|
||||
BOOL shouldBeEnabled = sender.isOn;
|
||||
|
||||
if (shouldBeEnabled == OWSScreenLock.sharedManager.isScreenLockEnabled) {
|
||||
DDLogError(@"%@ ignoring redundant screen lock.", self.logTag);
|
||||
return;
|
||||
}
|
||||
|
||||
DDLogInfo(@"%@ trying to set is screen lock enabled: %@", self.logTag, @(shouldBeEnabled));
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
if (shouldBeEnabled) {
|
||||
[OWSScreenLock.sharedManager tryToEnableScreenLockWithCompletion:^(NSError *_Nullable error) {
|
||||
[weakSelf updateTableContents];
|
||||
|
||||
if (error) {
|
||||
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"SCREEN_LOCK_ENABLE_FAILED",
|
||||
@"Title for alert indicating that screen lock could not be enabled.")
|
||||
message:error.localizedDescription];
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
[OWSScreenLock.sharedManager tryToDisableScreenLockWithCompletion:^(NSError *_Nullable error) {
|
||||
[weakSelf updateTableContents];
|
||||
|
||||
if (error) {
|
||||
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"SCREEN_LOCK_DISABLE_FAILED",
|
||||
@"Title for alert indicating that screen lock could not be disabled.")
|
||||
message:error.localizedDescription];
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)screenLockDidChange:(NSNotification *)notification
|
||||
{
|
||||
DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)showScreenLockTimeoutUI
|
||||
{
|
||||
DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
UIAlertController *controller = [UIAlertController
|
||||
alertControllerWithTitle:NSLocalizedString(@"SETTINGS_SCREEN_LOCK_ACTIVITY_TIMEOUT",
|
||||
@"Label for the 'screen lock activity timeout' setting of the privacy settings.")
|
||||
message:nil
|
||||
preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
for (NSNumber *timeoutValue in OWSScreenLock.sharedManager.screenLockTimeouts) {
|
||||
uint32_t screenLockTimeout = (uint32_t)round(timeoutValue.doubleValue);
|
||||
NSString *screenLockTimeoutString = [self formatScreenLockTimeout:screenLockTimeout];
|
||||
|
||||
[controller addAction:[UIAlertAction actionWithTitle:screenLockTimeoutString
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[OWSScreenLock.sharedManager
|
||||
setScreenLockTimeout:screenLockTimeout];
|
||||
}]];
|
||||
}
|
||||
[controller addAction:[OWSAlerts cancelAction]];
|
||||
UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController];
|
||||
[fromViewController presentViewController:controller animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (NSString *)formatScreenLockTimeout:(NSInteger)value
|
||||
{
|
||||
if (value <= 1) {
|
||||
return NSLocalizedString(@"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE",
|
||||
@"Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately.");
|
||||
}
|
||||
return [NSString formatDurationSeconds:(uint32_t)value useShortFormat:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
295
Signal/src/util/OWSScreenLock.swift
Normal file
295
Signal/src/util/OWSScreenLock.swift
Normal file
|
@ -0,0 +1,295 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import LocalAuthentication
|
||||
|
||||
@objc public class OWSScreenLock: NSObject {
|
||||
|
||||
public enum OWSScreenLockOutcome {
|
||||
case success
|
||||
case cancel
|
||||
// case userCancel
|
||||
// case otherCancel
|
||||
case failure(error:String)
|
||||
// case permanentFailure(error:String)
|
||||
}
|
||||
|
||||
@objc public let screenLockTimeouts = [
|
||||
5 * kSecondInterval,
|
||||
15 * kSecondInterval,
|
||||
30 * kSecondInterval,
|
||||
1 * kMinuteInterval,
|
||||
5 * kMinuteInterval,
|
||||
0
|
||||
]
|
||||
|
||||
@objc public static let ScreenLockDidChange = Notification.Name("ScreenLockDidChange")
|
||||
|
||||
let primaryStorage: OWSPrimaryStorage
|
||||
let dbConnection: YapDatabaseConnection
|
||||
|
||||
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
|
||||
|
||||
@objc(sharedManager)
|
||||
public static let shared = OWSScreenLock()
|
||||
|
||||
private override init() {
|
||||
self.primaryStorage = OWSPrimaryStorage.shared()
|
||||
self.dbConnection = self.primaryStorage.newDatabaseConnection()
|
||||
|
||||
super.init()
|
||||
|
||||
SwiftSingletons.register(self)
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@objc public func isScreenLockEnabled() -> Bool {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
if !OWSStorage.isStorageReady() {
|
||||
owsFail("\(logTag) 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("\(logTag) accessed screen lock state before storage is ready.")
|
||||
return 0
|
||||
}
|
||||
|
||||
let defaultTimeout = screenLockTimeouts[0]
|
||||
return self.dbConnection.double(forKey: OWSScreenLock_Key_ScreenLockTimeoutSeconds, inCollection: OWSScreenLock_Collection, defaultValue: defaultTimeout)
|
||||
}
|
||||
|
||||
@objc public func setScreenLockTimeout(_ 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
|
||||
|
||||
// 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.
|
||||
@objc public func tryToEnableScreenLock(completion: @escaping ((Error?) -> Void)) {
|
||||
tryToVerifyLocalAuthentication(localizedReason: NSLocalizedString("SCREEN_LOCK_REASON_ENABLE_SCREEN_LOCK",
|
||||
comment: "Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to enable 'screen lock'."),
|
||||
completion: { (outcome: OWSScreenLockOutcome) in
|
||||
AssertIsOnMainThread()
|
||||
|
||||
switch outcome {
|
||||
case .failure(let error):
|
||||
completion(self.authenticationError(errorDescription: error))
|
||||
case .success:
|
||||
self.setIsScreenLockEnabled(value: true)
|
||||
completion(nil)
|
||||
case .cancel:
|
||||
completion(nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 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.
|
||||
@objc public func tryToDisableScreenLock(completion: @escaping ((Error?) -> Void)) {
|
||||
tryToVerifyLocalAuthentication(localizedReason: NSLocalizedString("SCREEN_LOCK_REASON_DISABLE_SCREEN_LOCK",
|
||||
comment: "Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to disable 'screen lock'."),
|
||||
completion: { (outcome: OWSScreenLockOutcome) in
|
||||
AssertIsOnMainThread()
|
||||
|
||||
switch outcome {
|
||||
case .failure(let error):
|
||||
completion(self.authenticationError(errorDescription: error))
|
||||
case .success:
|
||||
self.setIsScreenLockEnabled(value: false)
|
||||
completion(nil)
|
||||
case .cancel:
|
||||
completion(nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@objc public func tryToUnlockScreenLock(success: @escaping (() -> Void),
|
||||
failure: @escaping ((Error) -> Void),
|
||||
cancel: @escaping (() -> Void)) {
|
||||
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'."),
|
||||
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
|
||||
// isScreenLockEnabled.
|
||||
private func tryToVerifyLocalAuthentication(localizedReason: String,
|
||||
completion completionParam: @escaping ((OWSScreenLockOutcome) -> Void)) {
|
||||
|
||||
// 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)
|
||||
if !canEvaluatePolicy || authError != nil {
|
||||
Logger.error("\(logTag) could not determine if local authentication is supported: \(String(describing: authError))")
|
||||
|
||||
let outcome = self.outcomeForLAError(errorParam: authError,
|
||||
defaultErrorDescription: defaultErrorDescription)
|
||||
switch outcome {
|
||||
case .success:
|
||||
owsFail("\(self.logTag) local authentication unexpected success")
|
||||
completion(.failure(error:defaultErrorDescription))
|
||||
case .cancel, .failure:
|
||||
completion(outcome)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: localizedReason) { success, evaluateError in
|
||||
if success {
|
||||
Logger.info("\(self.logTag) local authentication succeeded.")
|
||||
completion(.success)
|
||||
} else {
|
||||
let outcome = self.outcomeForLAError(errorParam: evaluateError,
|
||||
defaultErrorDescription: defaultErrorDescription)
|
||||
switch outcome {
|
||||
case .success:
|
||||
owsFail("\(self.logTag) local authentication unexpected success")
|
||||
completion(.failure(error:defaultErrorDescription))
|
||||
case .cancel, .failure:
|
||||
completion(outcome)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Outcome
|
||||
|
||||
private func outcomeForLAError(errorParam: Error?, defaultErrorDescription: String) -> OWSScreenLockOutcome {
|
||||
if let error = errorParam {
|
||||
guard let laError = error as? LAError else {
|
||||
return .failure(error:defaultErrorDescription)
|
||||
}
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
switch laError.code {
|
||||
case .biometryNotAvailable:
|
||||
Logger.error("\(self.logTag) local authentication error: biometryNotAvailable.")
|
||||
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_AVAILABLE",
|
||||
comment: "Indicates that Touch ID/Face ID/Phone Passcode are not available on this device."))
|
||||
case .biometryNotEnrolled:
|
||||
Logger.error("\(self.logTag) local authentication error: biometryNotEnrolled.")
|
||||
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_ENROLLED",
|
||||
comment: "Indicates that Touch ID/Face ID/Phone Passcode is not configured on this device."))
|
||||
case .biometryLockout:
|
||||
Logger.error("\(self.logTag) local authentication error: biometryLockout.")
|
||||
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_LOCKOUT",
|
||||
comment: "Indicates that Touch ID/Face ID/Phone Passcode is 'locked out' on this device due to authentication failures."))
|
||||
default:
|
||||
// Fall through to second switch
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
switch laError.code {
|
||||
case .authenticationFailed:
|
||||
Logger.error("\(self.logTag) local authentication error: authenticationFailed.")
|
||||
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_FAILED",
|
||||
comment: "Indicates that Touch ID/Face ID/Phone Passcode authentication failed."))
|
||||
case .userCancel, .userFallback, .systemCancel, .appCancel:
|
||||
Logger.info("\(self.logTag) local authentication cancelled.")
|
||||
return .cancel
|
||||
case .passcodeNotSet:
|
||||
Logger.error("\(self.logTag) local authentication error: passcodeNotSet.")
|
||||
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_PASSCODE_NOT_SET",
|
||||
comment: "Indicates that Touch ID/Face ID/Phone Passcode passcode is not set."))
|
||||
case .touchIDNotAvailable:
|
||||
Logger.error("\(self.logTag) local authentication error: touchIDNotAvailable.")
|
||||
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_AVAILABLE",
|
||||
comment: "Indicates that Touch ID/Face ID/Phone Passcode are not available on this device."))
|
||||
case .touchIDNotEnrolled:
|
||||
Logger.error("\(self.logTag) local authentication error: touchIDNotEnrolled.")
|
||||
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_ENROLLED",
|
||||
comment: "Indicates that Touch ID/Face ID/Phone Passcode is not configured on this device."))
|
||||
case .touchIDLockout:
|
||||
Logger.error("\(self.logTag) local authentication error: touchIDLockout.")
|
||||
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_LOCKOUT",
|
||||
comment: "Indicates that Touch ID/Face ID/Phone Passcode is 'locked out' on this device due to authentication failures."))
|
||||
case .invalidContext:
|
||||
owsFail("\(self.logTag) context not valid.")
|
||||
break
|
||||
case .notInteractive:
|
||||
owsFail("\(self.logTag) context not interactive.")
|
||||
break
|
||||
}
|
||||
}
|
||||
return .failure(error:defaultErrorDescription)
|
||||
}
|
||||
|
||||
private func authenticationError(errorDescription: String) -> Error {
|
||||
return OWSErrorWithCodeDescription(.localAuthenticationError,
|
||||
errorDescription)
|
||||
}
|
||||
|
||||
// MARK: - Context
|
||||
|
||||
private func screenLockContext() -> LAContext {
|
||||
let context = LAContext()
|
||||
|
||||
context.touchIDAuthenticationAllowableReuseDuration = TimeInterval(screenLockTimeout())
|
||||
|
||||
return context
|
||||
}
|
||||
}
|
17
Signal/src/util/OWSScreenLockUI.h
Normal file
17
Signal/src/util/OWSScreenLockUI.h
Normal file
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSScreenLockUI : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
+ (instancetype)sharedManager;
|
||||
|
||||
- (void)setupWithRootWindow:(UIWindow *)rootWindow;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
252
Signal/src/util/OWSScreenLockUI.m
Normal file
252
Signal/src/util/OWSScreenLockUI.m
Normal file
|
@ -0,0 +1,252 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSScreenLockUI.h"
|
||||
#import "Signal-Swift.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSScreenLockUI ()
|
||||
|
||||
// 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 -
|
||||
|
||||
@implementation OWSScreenLockUI
|
||||
|
||||
+ (instancetype)sharedManager
|
||||
{
|
||||
static OWSScreenLockUI *instance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [[self alloc] initDefault];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (instancetype)initDefault
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
[self observeNotifications];
|
||||
|
||||
OWSSingletonAssert();
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)observeNotifications
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(applicationDidBecomeActive:)
|
||||
name:OWSApplicationDidBecomeActiveNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(applicationWillResignActive:)
|
||||
name:OWSApplicationWillResignActiveNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(registrationStateDidChange)
|
||||
name:RegistrationStateDidChangeNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(screenLockDidChange:)
|
||||
name:OWSScreenLock.ScreenLockDidChange
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)setupWithRootWindow:(UIWindow *)rootWindow
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssert(rootWindow);
|
||||
|
||||
[self prepareScreenProtectionWithRootWindow:rootWindow];
|
||||
|
||||
[AppReadiness runNowOrWhenAppIsReady:^{
|
||||
[self ensureScreenProtection];
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Methods
|
||||
|
||||
- (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;
|
||||
}
|
||||
|
||||
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.
|
||||
shouldHaveScreenLock = NO;
|
||||
} else {
|
||||
// Otherwise, show 'Screen Lock'.
|
||||
shouldHaveScreenLock = YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Show 'Screen Protection' if:
|
||||
//
|
||||
// * App is inactive and...
|
||||
// * Either 'Screen Protection' or 'Screen Lock' is enabled.
|
||||
BOOL shouldHaveScreenProtection = (self.appIsInactive
|
||||
&& (Environment.preferences.screenSecurityIsEnabled || OWSScreenLock.sharedManager.isScreenLockEnabled));
|
||||
|
||||
BOOL shouldShowBlockWindow = shouldHaveScreenProtection || shouldHaveScreenLock;
|
||||
DDLogVerbose(@"%@, shouldHaveScreenProtection: %d, shouldHaveScreenLock: %d, shouldShowBlockWindow: %d",
|
||||
self.logTag,
|
||||
shouldHaveScreenProtection,
|
||||
shouldHaveScreenLock,
|
||||
shouldShowBlockWindow);
|
||||
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;
|
||||
|
||||
// Re-show the unlock UI.
|
||||
[self ensureScreenProtection];
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (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];
|
||||
}];
|
||||
}
|
||||
|
||||
// 'Screen Blocking' window obscures the app screen:
|
||||
//
|
||||
// * In the app switcher.
|
||||
// * During 'Screen Lock' unlock process.
|
||||
- (void)prepareScreenProtectionWithRootWindow:(UIWindow *)rootWindow
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssert(rootWindow);
|
||||
|
||||
UIWindow *window = [[UIWindow alloc] initWithFrame:rootWindow.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;
|
||||
}
|
||||
|
||||
#pragma mark - Events
|
||||
|
||||
- (void)screenLockDidChange:(NSNotification *)notification
|
||||
{
|
||||
[self ensureScreenProtection];
|
||||
}
|
||||
|
||||
- (void)registrationStateDidChange
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
DDLogInfo(@"registrationStateDidChange");
|
||||
|
||||
[self ensureScreenProtection];
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification
|
||||
{
|
||||
self.appIsInactive = NO;
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(NSNotification *)notification
|
||||
{
|
||||
self.appIsInactive = YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1537,6 +1537,45 @@
|
|||
/* Title for the 'scan QR code' view. */
|
||||
"SCAN_QR_CODE_VIEW_TITLE" = "Scan QR Code";
|
||||
|
||||
/* Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately. */
|
||||
"SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE" = "None";
|
||||
|
||||
/* Title for alert indicating that screen lock could not be disabled. */
|
||||
"SCREEN_LOCK_DISABLE_FAILED" = "Screen Lock Could Not Be Disabled";
|
||||
|
||||
/* Title for alert indicating that screen lock could not be enabled. */
|
||||
"SCREEN_LOCK_ENABLE_FAILED" = "Screen Lock Could Not Be Enabled";
|
||||
|
||||
/* Indicates that an unknown error occurred while using Touch ID/Face ID/Phone Passcode. */
|
||||
"SCREEN_LOCK_ENABLE_UNKNOWN_ERROR" = "Authentication could not be accessed.";
|
||||
|
||||
/* Indicates that Touch ID/Face ID/Phone Passcode authentication failed. */
|
||||
"SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_FAILED" = "Authentication failed.";
|
||||
|
||||
/* Indicates that Touch ID/Face ID/Phone Passcode is 'locked out' on this device due to authentication failures. */
|
||||
"SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_LOCKOUT" = "Too many failures. Please try again later.";
|
||||
|
||||
/* Indicates that Touch ID/Face ID/Phone Passcode are not available on this device. */
|
||||
"SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_AVAILABLE" = "You must configure a passcode on your phone to use Screen Lock.";
|
||||
|
||||
/* Indicates that Touch ID/Face ID/Phone Passcode is not configured on this device. */
|
||||
"SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_ENROLLED" = "You must configure a passcode on your phone to use Screen Lock.";
|
||||
|
||||
/* Indicates that Touch ID/Face ID/Phone Passcode passcode is not set. */
|
||||
"SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_PASSCODE_NOT_SET" = "You must configure a passcode on your phone to use Screen Lock.";
|
||||
|
||||
/* Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to disable 'screen lock'. */
|
||||
"SCREEN_LOCK_REASON_DISABLE_SCREEN_LOCK" = "Authenticate to disable Screen Lock.";
|
||||
|
||||
/* Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to enable 'screen lock'. */
|
||||
"SCREEN_LOCK_REASON_ENABLE_SCREEN_LOCK" = "Authenticate to lock access to Signal.";
|
||||
|
||||
/* Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'. */
|
||||
"SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK" = "Authenticate to access Signal.";
|
||||
|
||||
/* Title for alert indicating that screen lock could not be unlocked. */
|
||||
"SCREEN_LOCK_UNLOCK_FAILED" = "Authentication Failed";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"SEARCH_BYNAMEORNUMBER_PLACEHOLDER_TEXT" = "Search by name or number";
|
||||
|
||||
|
@ -1747,6 +1786,18 @@
|
|||
/* Remove metadata section header */
|
||||
"SETTINGS_REMOVE_METADATA_TITLE" = "Metadata";
|
||||
|
||||
/* Label for the 'screen lock activity timeout' setting of the privacy settings. */
|
||||
"SETTINGS_SCREEN_LOCK_ACTIVITY_TIMEOUT" = "Screen Lock Timeout";
|
||||
|
||||
/* Footer for the 'screen lock' section of the privacy settings. */
|
||||
"SETTINGS_SCREEN_LOCK_SECTION_FOOTER" = "Lock Signal when it hasn't been used recently. Unlock it with Touch ID, Face ID or your passcode.";
|
||||
|
||||
/* Title for the 'screen lock' section of the privacy settings. */
|
||||
"SETTINGS_SCREEN_LOCK_SECTION_TITLE" = "Screen Lock";
|
||||
|
||||
/* Label for the 'enable screen lock' switch of the privacy settings. */
|
||||
"SETTINGS_SCREEN_LOCK_SWITCH_LABEL" = "Screen Lock";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"SETTINGS_SCREEN_SECURITY" = "Enable Screen Security";
|
||||
|
||||
|
|
|
@ -21,4 +21,5 @@
|
|||
#import <SignalServiceKit/OWSDispatch.h>
|
||||
#import <SignalServiceKit/iOSVersions.h>
|
||||
#import <SignalServiceKit/NSObject+OWS.h>
|
||||
#import <SignalServiceKit/NSString+SSK.h>
|
||||
#endif
|
||||
|
|
|
@ -57,7 +57,7 @@ public class DisappearingTimerConfigurationView: UIView {
|
|||
imageView.contentMode = .scaleAspectFit
|
||||
|
||||
self.label = UILabel()
|
||||
label.text = OWSDisappearingMessagesConfiguration.string(forDurationSeconds: durationSeconds, useShortFormat: true)
|
||||
label.text = NSString.formatDurationSeconds(durationSeconds, useShortFormat: true)
|
||||
label.font = UIFont.systemFont(ofSize: 10)
|
||||
label.textColor = UIColor.white
|
||||
label.textAlignment = .center
|
||||
|
@ -81,7 +81,7 @@ public class DisappearingTimerConfigurationView: UIView {
|
|||
// Accessability
|
||||
self.accessibilityLabel = NSLocalizedString("DISAPPEARING_MESSAGES_LABEL", comment: "Accessibility label for disappearing messages")
|
||||
let hintFormatString = NSLocalizedString("DISAPPEARING_MESSAGES_HINT", comment: "Accessibility hint that contains current timeout information")
|
||||
let durationString = OWSDisappearingMessagesConfiguration.string(forDurationSeconds: durationSeconds, useShortFormat: false)
|
||||
let durationString = NSString.formatDurationSeconds(durationSeconds, useShortFormat: false)
|
||||
self.accessibilityHint = String(format: hintFormatString, durationString)
|
||||
|
||||
// Layout
|
||||
|
|
|
@ -25,8 +25,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
+ (NSArray<NSNumber *> *)validDurationsSeconds;
|
||||
|
||||
+ (NSString *)stringForDurationSeconds:(uint32_t)durationSeconds useShortFormat:(BOOL)useShortFormat;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#import "OWSDisappearingMessagesConfiguration.h"
|
||||
#import "NSDate+OWS.h"
|
||||
#import "NSString+SSK.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@ -59,120 +60,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
}
|
||||
|
||||
+ (NSString *)stringForDurationSeconds:(uint32_t)durationSeconds useShortFormat:(BOOL)useShortFormat
|
||||
{
|
||||
NSString *amountFormat;
|
||||
uint32_t duration;
|
||||
|
||||
uint32_t secondsPerMinute = 60;
|
||||
uint32_t secondsPerHour = secondsPerMinute * 60;
|
||||
uint32_t secondsPerDay = secondsPerHour * 24;
|
||||
uint32_t secondsPerWeek = secondsPerDay * 7;
|
||||
|
||||
if (durationSeconds < secondsPerMinute) { // XX Seconds
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_SECONDS_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of seconds}}. Must be very short, like 1 or 2 characters, The space is intentionally ommitted between the text and the embedded duration so that we get, e.g. '5s' not '5 s'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_SECONDS",
|
||||
@"{{number of seconds}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{5 seconds}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds;
|
||||
} else if (durationSeconds < secondsPerMinute * 1.5) { // 1 Minute
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_MINUTES_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of minutes}}. Must be very short, like 1 or 2 characters, The space is intentionally ommitted between the text and the embedded duration so that we get, e.g. '5m' not '5 m'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_SINGLE_MINUTE",
|
||||
@"{{1 minute}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{1 minute}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
duration = durationSeconds / secondsPerMinute;
|
||||
} else if (durationSeconds < secondsPerHour) { // Multiple Minutes
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_MINUTES_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of minutes}}. Must be very short, like 1 or 2 characters, The space is intentionally ommitted between the text and the embedded duration so that we get, e.g. '5m' not '5 m'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_MINUTES",
|
||||
@"{{number of minutes}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{5 minutes}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds / secondsPerMinute;
|
||||
} else if (durationSeconds < secondsPerHour * 1.5) { // 1 Hour
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_HOURS_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of hours}}. Must be very short, like 1 or 2 characters, The space is intentionally ommitted between the text and the embedded duration so that we get, e.g. '5h' not '5 h'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_SINGLE_HOUR",
|
||||
@"{{1 hour}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{1 hour}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds / secondsPerHour;
|
||||
} else if (durationSeconds < secondsPerDay) { // Multiple Hours
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_HOURS_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of hours}}. Must be very short, like 1 or 2 characters, The space is intentionally ommitted between the text and the embedded duration so that we get, e.g. '5h' not '5 h'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_HOURS",
|
||||
@"{{number of hours}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{5 hours}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds / secondsPerHour;
|
||||
} else if (durationSeconds < secondsPerDay * 1.5) { // 1 Day
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_DAYS_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of days}}. Must be very short, like 1 or 2 characters, The space is intentionally ommitted between the text and the embedded duration so that we get, e.g. '5d' not '5 d'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_SINGLE_DAY",
|
||||
@"{{1 day}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{1 day}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds / secondsPerDay;
|
||||
} else if (durationSeconds < secondsPerWeek) { // Multiple Days
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_DAYS_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of days}}. Must be very short, like 1 or 2 characters, The space is intentionally ommitted between the text and the embedded duration so that we get, e.g. '5d' not '5 d'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_DAYS",
|
||||
@"{{number of days}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{5 days}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds / secondsPerDay;
|
||||
} else if (durationSeconds < secondsPerWeek * 1.5) { // 1 Week
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_WEEKS_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of weeks}}. Must be very short, like 1 or 2 characters, The space is intentionally ommitted between the text and the embedded duration so that we get, e.g. '5w' not '5 w'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_SINGLE_WEEK",
|
||||
@"{{1 week}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{1 week}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds / secondsPerWeek;
|
||||
} else { // Multiple weeks
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_WEEKS_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of weeks}}. Must be very short, like 1 or 2 characters, The space is intentionally ommitted between the text and the embedded duration so that we get, e.g. '5w' not '5 w'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_WEEKS",
|
||||
@"{{number of weeks}}, embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{5 weeks}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds / secondsPerWeek;
|
||||
}
|
||||
|
||||
return [NSString stringWithFormat:amountFormat, [NSNumberFormatter localizedStringFromNumber:@(duration)
|
||||
numberStyle:NSNumberFormatterNoStyle]];
|
||||
}
|
||||
|
||||
+ (NSArray<NSNumber *> *)validDurationsSeconds
|
||||
{
|
||||
return @[ @(5),
|
||||
|
@ -195,7 +82,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (NSString *)durationString
|
||||
{
|
||||
return [self.class stringForDurationSeconds:self.durationSeconds useShortFormat:NO];
|
||||
return [NSString formatDurationSeconds:self.durationSeconds useShortFormat:NO];
|
||||
}
|
||||
|
||||
#pragma mark - Dirty Tracking
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
//
|
||||
|
||||
#import "OWSDisappearingConfigurationUpdateInfoMessage.h"
|
||||
#import "NSString+SSK.h"
|
||||
#import "OWSDisappearingMessagesConfiguration.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
@ -58,7 +59,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
@"strings for context.");
|
||||
|
||||
NSString *durationString =
|
||||
[OWSDisappearingMessagesConfiguration stringForDurationSeconds:self.configurationDurationSeconds useShortFormat:NO];
|
||||
[NSString formatDurationSeconds:self.configurationDurationSeconds useShortFormat:NO];
|
||||
return [NSString stringWithFormat:infoFormat, self.createdByRemoteName, durationString];
|
||||
} else {
|
||||
NSString *infoFormat = NSLocalizedString(@"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION",
|
||||
|
@ -71,7 +72,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
@"Info message embedding a {{time amount}}, see the *_TIME_AMOUNT strings for context.");
|
||||
|
||||
NSString *durationString =
|
||||
[OWSDisappearingMessagesConfiguration stringForDurationSeconds:self.configurationDurationSeconds useShortFormat:NO];
|
||||
[NSString formatDurationSeconds:self.configurationDurationSeconds useShortFormat:NO];
|
||||
return [NSString stringWithFormat:infoFormat, durationString];
|
||||
} else {
|
||||
return NSLocalizedString(@"YOU_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION",
|
||||
|
|
|
@ -29,4 +29,5 @@ extern NSString *const kNSNotificationName_BlockedPhoneNumbersDidChange;
|
|||
|
||||
@end
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// These NSTimeInterval constants provide simplified durations for readability.
|
||||
extern const NSTimeInterval kSecondInterval;
|
||||
extern const NSTimeInterval kMinuteInterval;
|
||||
extern const NSTimeInterval kHourInterval;
|
||||
extern const NSTimeInterval kDayInterval;
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
||||
const NSTimeInterval kSecondInterval = 1;
|
||||
const NSTimeInterval kMinuteInterval = 60;
|
||||
const NSTimeInterval kHourInterval = 60 * kMinuteInterval;
|
||||
const NSTimeInterval kDayInterval = 24 * kHourInterval;
|
||||
|
|
|
@ -14,6 +14,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (BOOL)isValidE164;
|
||||
|
||||
+ (NSString *)formatDurationSeconds:(uint32_t)durationSeconds useShortFormat:(BOOL)useShortFormat;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -234,6 +234,138 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return [regex rangeOfFirstMatchInString:self options:0 range:NSMakeRange(0, self.length)].location != NSNotFound;
|
||||
}
|
||||
|
||||
+ (NSString *)formatDurationSeconds:(uint32_t)durationSeconds useShortFormat:(BOOL)useShortFormat
|
||||
{
|
||||
NSString *amountFormat;
|
||||
uint32_t duration;
|
||||
|
||||
uint32_t secondsPerMinute = 60;
|
||||
uint32_t secondsPerHour = secondsPerMinute * 60;
|
||||
uint32_t secondsPerDay = secondsPerHour * 24;
|
||||
uint32_t secondsPerWeek = secondsPerDay * 7;
|
||||
|
||||
if (durationSeconds < secondsPerMinute) { // XX Seconds
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_SECONDS_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of seconds}}. Must be very short, like 1 or 2 "
|
||||
@"characters, The space is intentionally ommitted between the text and the embedded duration so that "
|
||||
@"we get, e.g. '5s' not '5 s'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_SECONDS",
|
||||
@"{{number of seconds}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{5 seconds}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds;
|
||||
} else if (durationSeconds < secondsPerMinute * 1.5) { // 1 Minute
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_MINUTES_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of minutes}}. Must be very short, like 1 or 2 "
|
||||
@"characters, The space is intentionally ommitted between the text and the embedded duration so that "
|
||||
@"we get, e.g. '5m' not '5 m'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_SINGLE_MINUTE",
|
||||
@"{{1 minute}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{1 minute}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
duration = durationSeconds / secondsPerMinute;
|
||||
} else if (durationSeconds < secondsPerHour) { // Multiple Minutes
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_MINUTES_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of minutes}}. Must be very short, like 1 or 2 "
|
||||
@"characters, The space is intentionally ommitted between the text and the embedded duration so that "
|
||||
@"we get, e.g. '5m' not '5 m'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_MINUTES",
|
||||
@"{{number of minutes}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{5 minutes}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds / secondsPerMinute;
|
||||
} else if (durationSeconds < secondsPerHour * 1.5) { // 1 Hour
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_HOURS_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of hours}}. Must be very short, like 1 or 2 "
|
||||
@"characters, The space is intentionally ommitted between the text and the embedded duration so that "
|
||||
@"we get, e.g. '5h' not '5 h'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_SINGLE_HOUR",
|
||||
@"{{1 hour}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{1 hour}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds / secondsPerHour;
|
||||
} else if (durationSeconds < secondsPerDay) { // Multiple Hours
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_HOURS_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of hours}}. Must be very short, like 1 or 2 "
|
||||
@"characters, The space is intentionally ommitted between the text and the embedded duration so that "
|
||||
@"we get, e.g. '5h' not '5 h'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_HOURS",
|
||||
@"{{number of hours}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{5 hours}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds / secondsPerHour;
|
||||
} else if (durationSeconds < secondsPerDay * 1.5) { // 1 Day
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_DAYS_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of days}}. Must be very short, like 1 or 2 "
|
||||
@"characters, The space is intentionally ommitted between the text and the embedded duration so that "
|
||||
@"we get, e.g. '5d' not '5 d'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_SINGLE_DAY",
|
||||
@"{{1 day}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{1 day}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds / secondsPerDay;
|
||||
} else if (durationSeconds < secondsPerWeek) { // Multiple Days
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_DAYS_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of days}}. Must be very short, like 1 or 2 "
|
||||
@"characters, The space is intentionally ommitted between the text and the embedded duration so that "
|
||||
@"we get, e.g. '5d' not '5 d'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_DAYS",
|
||||
@"{{number of days}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{5 days}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds / secondsPerDay;
|
||||
} else if (durationSeconds < secondsPerWeek * 1.5) { // 1 Week
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_WEEKS_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of weeks}}. Must be very short, like 1 or 2 "
|
||||
@"characters, The space is intentionally ommitted between the text and the embedded duration so that "
|
||||
@"we get, e.g. '5w' not '5 w'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_SINGLE_WEEK",
|
||||
@"{{1 week}} embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{1 week}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds / secondsPerWeek;
|
||||
} else { // Multiple weeks
|
||||
if (useShortFormat) {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_WEEKS_SHORT_FORMAT",
|
||||
@"Label text below navbar button, embeds {{number of weeks}}. Must be very short, like 1 or 2 "
|
||||
@"characters, The space is intentionally ommitted between the text and the embedded duration so that "
|
||||
@"we get, e.g. '5w' not '5 w'. See other *_TIME_AMOUNT strings");
|
||||
} else {
|
||||
amountFormat = NSLocalizedString(@"TIME_AMOUNT_WEEKS",
|
||||
@"{{number of weeks}}, embedded in strings, e.g. 'Alice updated disappearing messages "
|
||||
@"expiration to {{5 weeks}}'. See other *_TIME_AMOUNT strings");
|
||||
}
|
||||
|
||||
duration = durationSeconds / secondsPerWeek;
|
||||
}
|
||||
|
||||
return [NSString stringWithFormat:amountFormat,
|
||||
[NSNumberFormatter localizedStringFromNumber:@(duration) numberStyle:NSNumberFormatterNoStyle]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -41,6 +41,7 @@ typedef NS_ENUM(NSInteger, OWSErrorCode) {
|
|||
OWSErrorCodeImportBackupError = 777418,
|
||||
// A non-recoverable while importing or exporting a backup.
|
||||
OWSErrorCodeBackupFailure = 777419,
|
||||
OWSErrorCodeLocalAuthenticationError = 777420,
|
||||
};
|
||||
|
||||
extern NSString *const OWSErrorRecipientIdentifierKey;
|
||||
|
|
Loading…
Reference in a new issue