From 980b3d25a7204f712ecbb54a709649ca3ad3aa98 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 5 Jan 2018 14:42:50 -0500 Subject: [PATCH] Rework "export backup" UI. --- Signal.xcodeproj/project.pbxproj | 12 +- .../src/ViewControllers/DebugUI/DebugUIMisc.m | 21 +- .../ViewControllers/OWSBackupViewController.h | 19 ++ .../ViewControllers/OWSBackupViewController.m | 269 ++++++++++++++++++ Signal/src/util/OWSBackup.h | 31 +- Signal/src/util/OWSBackup.m | 166 +++++------ .../translations/en.lproj/Localizable.strings | 32 ++- .../ViewControllers/OWSTableViewController.h | 11 +- .../ViewControllers/OWSTableViewController.m | 44 ++- SignalMessaging/categories/UIView+OWS.h | 9 +- SignalMessaging/categories/UIView+OWS.m | 32 ++- SignalServiceKit/src/Util/MIMETypeUtil.m | 6 +- 12 files changed, 526 insertions(+), 126 deletions(-) create mode 100644 Signal/src/ViewControllers/OWSBackupViewController.h create mode 100644 Signal/src/ViewControllers/OWSBackupViewController.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 9133a7533..3a12f9332 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ 344F248720069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F248620069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift */; }; 344F248A20069F0600CFB4F4 /* ViewControllerUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 344F248820069F0600CFB4F4 /* ViewControllerUtils.h */; }; 344F248B20069F0600CFB4F4 /* ViewControllerUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 344F248920069F0600CFB4F4 /* ViewControllerUtils.m */; }; + 3456D2C41FFFCC70001EA55D /* OWSBackupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3456D2C21FFFCC6F001EA55D /* OWSBackupViewController.m */; }; 3461284B1FD0B94000532771 /* SAELoadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */; }; 346129341FD1A88700532771 /* OWSSwiftUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346129331FD1A88700532771 /* OWSSwiftUtils.swift */; }; 346129391FD1B47300532771 /* OWSPreferences.h in Headers */ = {isa = PBXBuildFile; fileRef = 346129371FD1B47200532771 /* OWSPreferences.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -523,6 +524,8 @@ 344F248920069F0600CFB4F4 /* ViewControllerUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ViewControllerUtils.m; path = SignalMessaging/contacts/ViewControllerUtils.m; sourceTree = SOURCE_ROOT; }; 34533F161EA8D2070006114F /* OWSAudioAttachmentPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAudioAttachmentPlayer.h; sourceTree = ""; }; 34533F171EA8D2070006114F /* OWSAudioAttachmentPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAudioAttachmentPlayer.m; sourceTree = ""; }; + 3456D2C21FFFCC6F001EA55D /* OWSBackupViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupViewController.m; sourceTree = ""; }; + 3456D2C31FFFCC6F001EA55D /* OWSBackupViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupViewController.h; sourceTree = ""; }; 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAELoadViewController.swift; sourceTree = ""; }; 346129331FD1A88700532771 /* OWSSwiftUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSSwiftUtils.swift; sourceTree = ""; }; 346129371FD1B47200532771 /* OWSPreferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSPreferences.h; sourceTree = ""; }; @@ -1323,6 +1326,8 @@ 34B3F8591E8DF1700035BE1A /* NotificationSettingsViewController.m */, 34CCAF391F0C2748004084F4 /* OWSAddToContactViewController.h */, 34CCAF3A1F0C2748004084F4 /* OWSAddToContactViewController.m */, + 3456D2C31FFFCC6F001EA55D /* OWSBackupViewController.h */, + 3456D2C21FFFCC6F001EA55D /* OWSBackupViewController.m */, 34B3F85B1E8DF1700035BE1A /* OWSConversationSettingsViewController.h */, 34B3F85C1E8DF1700035BE1A /* OWSConversationSettingsViewController.m */, 34D5CCAB1EAE7136005515DB /* OWSConversationSettingsViewDelegate.h */, @@ -2923,6 +2928,7 @@ 45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */, 34B3F8711E8DF1700035BE1A /* AboutTableViewController.m in Sources */, 34B3F88D1E8DF1700035BE1A /* OWSQRCodeScanningViewController.m in Sources */, + 3456D2C41FFFCC70001EA55D /* OWSBackupViewController.m in Sources */, 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */, 34CCAF3B1F0C2748004084F4 /* OWSAddToContactViewController.m in Sources */, 34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */, @@ -3114,7 +3120,11 @@ "DEBUG=1", "$(inherited)", ); - "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = "DEBUG=1 $(inherited) SSK_BUILDING_FOR_TESTS=1"; + "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = ( + "DEBUG=1", + "$(inherited)", + "SSK_BUILDING_FOR_TESTS=1", + ); GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m index b915111c3..aff68bf7f 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m @@ -4,6 +4,7 @@ #import "DebugUIMisc.h" #import "OWSBackup.h" +#import "OWSBackupViewController.h" #import "OWSCountryMetadata.h" #import "OWSTableViewController.h" #import "RegistrationViewController.h" @@ -94,10 +95,22 @@ NS_ASSUME_NONNULL_BEGIN [DebugUIMisc sendUnencryptedDatabase:thread]; }]]; } - [items addObject:[OWSTableItem itemWithTitle:@"Export Backup" - actionBlock:^{ - [OWSBackup exportBackup]; - }]]; + [items addObject:[OWSTableItem + subPageItemWithText:@"Export Backup w/ Password" + actionBlock:^(UIViewController *viewController) { + OWSBackupViewController *backupViewController = [OWSBackupViewController new]; + [backupViewController exportBackup:thread skipPassword:NO]; + [viewController.navigationController pushViewController:backupViewController + animated:YES]; + }]]; + [items addObject:[OWSTableItem + subPageItemWithText:@"Export Backup w/o Password" + actionBlock:^(UIViewController *viewController) { + OWSBackupViewController *backupViewController = [OWSBackupViewController new]; + [backupViewController exportBackup:thread skipPassword:YES]; + [viewController.navigationController pushViewController:backupViewController + animated:YES]; + }]]; return [OWSTableSection sectionWithTitle:self.name items:items]; } diff --git a/Signal/src/ViewControllers/OWSBackupViewController.h b/Signal/src/ViewControllers/OWSBackupViewController.h new file mode 100644 index 000000000..f4467ac81 --- /dev/null +++ b/Signal/src/ViewControllers/OWSBackupViewController.h @@ -0,0 +1,19 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class TSThread; + +@interface OWSBackupViewController : OWSViewController + +// If currentThread is non-nil, we should offer to let users send the +// backup in that thread. +- (void)exportBackup:(TSThread *_Nullable)currentThread skipPassword:(BOOL)skipPassword; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/OWSBackupViewController.m b/Signal/src/ViewControllers/OWSBackupViewController.m new file mode 100644 index 000000000..5bf508975 --- /dev/null +++ b/Signal/src/ViewControllers/OWSBackupViewController.m @@ -0,0 +1,269 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBackupViewController.h" +#import "OWSBackup.h" +#import "Signal-Swift.h" +#import "ThreadUtil.h" +#import +#import +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSBackupViewController () + +@property (nonatomic) OWSBackup *backup; + +@end + +#pragma mark - + +@implementation OWSBackupViewController + +- (void)loadView +{ + [super loadView]; + + self.view.backgroundColor = [UIColor whiteColor]; + + self.navigationItem.title = NSLocalizedString(@"BACKUP_EXPORT_VIEW_TITLE", @"Title for the 'backup export' view."); + self.navigationItem.leftBarButtonItem = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop + target:self + action:@selector(dismissWasPressed:)]; + + self.backup.delegate = self; + + [self updateUI]; +} + +- (void)exportBackup:(TSThread *_Nullable)currentThread skipPassword:(BOOL)skipPassword +{ + OWSAssertIsOnMainThread(); + + // We set ourselves as the delegate of the backup later, + // after we've loaded our view. + self.backup = [OWSBackup new]; + [self.backup exportBackup:currentThread skipPassword:skipPassword]; +} + +- (void)updateUI +{ + for (UIView *subview in self.view.subviews) { + [subview removeFromSuperview]; + } + + switch (self.backup.backupState) { + case OWSBackupState_InProgress: + [self showInProgressUI]; + break; + case OWSBackupState_Cancelled: + [self showCancelledUI]; + break; + case OWSBackupState_Complete: + [self showCompleteUI]; + break; + } +} + +- (void)showInProgressUI +{ + + UIView *container = [UIView new]; + [self.view addSubview:container]; + [container autoCenterInSuperview]; + + UIActivityIndicatorView *activityIndicatorView = + [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + [container addSubview:activityIndicatorView]; + [activityIndicatorView autoPinEdgeToSuperviewEdge:ALEdgeTop]; + [activityIndicatorView autoHCenterInSuperview]; + [activityIndicatorView startAnimating]; + + UILabel *label = [UILabel new]; + label.text = NSLocalizedString( + @"BACKUP_EXPORT_IN_PROGRESS_MESSAGE", @"Message indicating that backup export is in progress."); + label.textColor = [UIColor blackColor]; + label.font = [UIFont ows_regularFontWithSize:18.f]; + [container addSubview:label]; + [label autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:activityIndicatorView withOffset:20.f]; + [label autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + [label autoPinWidthToSuperview]; +} + +- (void)showCancelledUI +{ + // Show nothing. +} + +- (void)showCompleteUI +{ + NSMutableArray *subviews = [NSMutableArray new]; + + { + NSString *message = NSLocalizedString( + @"BACKUP_EXPORT_COMPLETE_MESSAGE", @"Message indicating that backup export without password is complete."); + + UILabel *label = [UILabel new]; + label.text = message; + label.textColor = [UIColor blackColor]; + label.font = [UIFont ows_regularFontWithSize:18.f]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + [subviews addObject:label]; + } + + if (self.backup.backupPassword) { + NSString *message = [NSString stringWithFormat:NSLocalizedString(@"BACKUP_EXPORT_PASSWORD_MESSAGE_FORMAT", + @"Format for message indicating that backup export with " + @"password is complete. Embeds: {{the backup password}}."), + self.backup.backupPassword]; + + UILabel *label = [UILabel new]; + label.text = message; + label.textColor = [UIColor blackColor]; + label.font = [UIFont ows_regularFontWithSize:14.f]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + [subviews addObject:label]; + } + + [subviews addObject:[UIView new]]; + + if (self.backup.backupPassword) { + [subviews + addObject:[self makeButtonWithTitle:NSLocalizedString(@"BACKUP_EXPORT_COPY_PASSWORD_BUTTON", + @"Label for button that copies backup password to the pasteboard.") + selector:@selector(copyPassword)]]; + } + + [subviews addObject:[self makeButtonWithTitle:NSLocalizedString(@"BACKUP_EXPORT_SHARE_BACKUP_BUTTON", + @"Label for button that opens share UI for backup.") + selector:@selector(shareBackup)]]; + + if (self.backup.currentThread) { + [subviews + addObject:[self makeButtonWithTitle:NSLocalizedString(@"BACKUP_EXPORT_SEND_BACKUP_BUTTON", + @"Label for button that 'send backup' in the current conversation.") + selector:@selector(sendBackup)]]; + } + + // TODO: We should offer the option to save the backup to "Files", iCloud, Dropbox, etc. + + UIView *container = [UIView verticalStackWithSubviews:subviews spacing:10]; + [self.view addSubview:container]; + [container autoVCenterInSuperview]; + [container autoPinWidthToSuperviewWithMargin:25.f]; +} + +- (UIView *)makeButtonWithTitle:(NSString *)title selector:(SEL)selector +{ + const CGFloat kButtonHeight = 40; + OWSFlatButton *button = [OWSFlatButton buttonWithTitle:title + font:[OWSFlatButton fontForHeight:kButtonHeight] + titleColor:[UIColor whiteColor] + backgroundColor:[UIColor ows_materialBlueColor] + target:self + selector:selector]; + [button autoSetDimension:ALDimensionWidth toSize:140]; + [button autoSetDimension:ALDimensionHeight toSize:kButtonHeight]; + return button; +} + +- (void)copyPassword +{ + OWSAssert(self.backup.backupPassword.length > 0); + + [UIPasteboard.generalPasteboard setString:self.backup.backupPassword]; +} + +- (void)shareBackup +{ + OWSAssertIsOnMainThread(); + OWSAssert(self.backup.backupZipPath.length > 0); + + [AttachmentSharing showShareUIForURL:[NSURL fileURLWithPath:self.backup.backupZipPath]]; +} + +- (void)sendBackup +{ + OWSAssertIsOnMainThread(); + OWSAssert(self.backup.backupZipPath.length > 0); + OWSAssert(self.backup.currentThread); + + [ModalActivityIndicatorViewController + presentFromViewController:self + canCancel:NO + backgroundBlock:^(ModalActivityIndicatorViewController *modalActivityIndicator) { + NSString *fileName = [self.backup.backupZipPath lastPathComponent]; + + OWSMessageSender *messageSender = [Environment current].messageSender; + NSString *utiType = [MIMETypeUtil utiTypeForFileExtension:fileName.pathExtension]; + DataSource *_Nullable dataSource = + [DataSourcePath dataSourceWithFilePath:self.backup.backupZipPath]; + [dataSource setSourceFilename:fileName]; + SignalAttachment *attachment = + [SignalAttachment attachmentWithDataSource:dataSource dataUTI:utiType]; + if (!attachment || [attachment hasError]) { + OWSFail(@"%@ attachment[%@]: %@", + self.logTag, + [attachment sourceFilename], + [attachment errorName]); + return; + } + dispatch_async(dispatch_get_main_queue(), ^{ + [ThreadUtil + sendMessageWithAttachment:attachment + inThread:self.backup.currentThread + messageSender:messageSender + completion:^(NSError *_Nullable error) { + + OWSAssertIsOnMainThread(); + [modalActivityIndicator dismissWithCompletion:^{ + if (error) { + DDLogError(@"%@ send backup failed: %@", self.logTag, error); + [OWSAlerts + showAlertWithTitle:NSLocalizedString( + @"BACKUP_EXPORT_SEND_BACKUP_FAILED", + @"Message indicating that sending " + @"the backup failed.")]; + } else { + [OWSAlerts + showAlertWithTitle: + NSLocalizedString(@"BACKUP_EXPORT_SEND_BACKUP_SUCCESS", + @"Message indicating that sending the backup " + @"succeeded.")]; + } + }]; + }]; + }); + }]; +} + +- (void)dismissWasPressed:(id)sender +{ + [self.backup cancel]; + + [self.navigationController popViewControllerAnimated:YES]; +} + +#pragma mark - OWSBackupDelegate + +- (void)backupStateDidChange +{ + DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__); + + [self updateUI]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/util/OWSBackup.h b/Signal/src/util/OWSBackup.h index 3c28653e0..c9e103b66 100644 --- a/Signal/src/util/OWSBackup.h +++ b/Signal/src/util/OWSBackup.h @@ -4,9 +4,38 @@ NS_ASSUME_NONNULL_BEGIN +@protocol OWSBackupDelegate + +- (void)backupStateDidChange; + +@end + +#pragma mark - + +typedef NS_ENUM(NSUInteger, OWSBackupState) { + OWSBackupState_InProgress, + OWSBackupState_Cancelled, + OWSBackupState_Complete, +}; + +@class TSThread; + @interface OWSBackup : NSObject -+ (void)exportBackup; +@property (nonatomic, weak) id delegate; + +@property (nonatomic) OWSBackupState backupState; + +// If non-nil, backup is encrypted. +@property (nonatomic, nullable, readonly) NSString *backupPassword; + +@property (nonatomic, nullable, readonly) TSThread *currentThread; + +@property (nonatomic, readonly) NSString *backupZipPath; + +- (void)exportBackup:(nullable TSThread *)currentThread skipPassword:(BOOL)skipPassword; + +- (void)cancel; @end diff --git a/Signal/src/util/OWSBackup.m b/Signal/src/util/OWSBackup.m index e011e5cbd..06d5dba52 100644 --- a/Signal/src/util/OWSBackup.m +++ b/Signal/src/util/OWSBackup.m @@ -7,7 +7,6 @@ #import "Signal-Swift.h" #import "zlib.h" #import -#import #import #import #import @@ -24,10 +23,12 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSBackup () -@property (nonatomic) NSString *password; +@property (nonatomic, nullable) TSThread *currentThread; + +@property (nonatomic, nullable) NSString *backupPassword; + @property (nonatomic) NSString *backupDirPath; @property (nonatomic) NSString *backupZipPath; -@property (atomic) BOOL cancelled; @end @@ -43,107 +44,58 @@ NS_ASSUME_NONNULL_BEGIN [OWSFileSystem deleteFileIfExists:self.backupDirPath]; } -+ (void)exportBackup +- (void)setBackupState:(OWSBackupState)backupState { - dispatch_async(dispatch_get_main_queue(), ^{ - [[OWSBackup new] exportBackup]; - }); + _backupState = backupState; + + [self.delegate backupStateDidChange]; } -- (void)exportBackup +- (void)cancel +{ + self.backupState = OWSBackupState_Cancelled; +} + +- (BOOL)isCancelled +{ + return self.backupState == OWSBackupState_Cancelled; +} + +- (void)exportBackup:(nullable TSThread *)currentThread skipPassword:(BOOL)skipPassword { OWSAssertIsOnMainThread(); - // TODO: Should the user pick a password? - // If not, should probably generate something more user-friendly, - // e.g. case-insensitive set of hexadecimal? - NSString *password = [NSUUID UUID].UUIDString; - self.password = password; - DDLogVerbose(@"%@ backup export complete; password: %@", self.logTag, password); + self.currentThread = currentThread; + self.backupState = OWSBackupState_InProgress; - [self showExportProgressUI:^(UIAlertController *exportProgressAlert) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self exportToFilesAndZip]; + if (skipPassword) { + DDLogVerbose(@"%@ backup export without password", self.logTag); + } else { + // TODO: Should the user pick a password? + // If not, should probably generate something more user-friendly, + // e.g. case-insensitive set of hexadecimal? + NSString *backupPassword = [NSUUID UUID].UUIDString; + self.backupPassword = backupPassword; + DDLogVerbose(@"%@ backup export with password: %@", self.logTag, backupPassword); + } - dispatch_async(dispatch_get_main_queue(), ^{ - [exportProgressAlert dismissViewControllerAnimated:YES - completion:^{ - [self showExportCompleteUI:^{ - [self showShareUI]; - }]; - }]; - }); + [self startExport]; +} + +- (void)startExport +{ + OWSAssertIsOnMainThread(); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self exportToFilesAndZip]; + + dispatch_async(dispatch_get_main_queue(), ^{ + if (!self.isCancelled) { + self.backupState = OWSBackupState_Complete; + } + [self.delegate backupStateDidChange]; }); - }]; -} - -- (void)showExportProgressUI:(void (^_Nonnull)(UIAlertController *))completion -{ - OWSAssertIsOnMainThread(); - OWSAssert(completion); - - NSString *title = NSLocalizedString( - @"BACKUP_EXPORT_IN_PROGRESS_ALERT_TITLE", @"Title for the 'backup export in progress' alert."); - UIAlertController *alert = - [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[CommonStrings cancelButton] - style:UIAlertActionStyleCancel - handler:^(UIAlertAction *_Nonnull action) { - self.cancelled = YES; - }]; - [alert addAction:cancelAction]; - - __weak UIAlertController *weakAlert = alert; - UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; - [fromViewController presentViewController:alert - animated:YES - completion:^(void) { - UIAlertController *strongAlert = weakAlert; - if (!strongAlert) { - OWSFail(@"%@ missing alert.", self.logTag); - return; - } - completion(strongAlert); - }]; -} - -- (void)showExportCompleteUI:(void (^_Nonnull)(void))completion -{ - OWSAssertIsOnMainThread(); - OWSAssert(completion); - OWSAssert(self.password.length > 0); - - // TODO: We probably want to offer an option that lets users copy - // the password to the pasteboard. - NSString *title - = NSLocalizedString(@"BACKUP_EXPORT_COMPLETE_ALERT_TITLE", @"Title for the 'backup export complete' alert."); - NSString *message = [NSString - stringWithFormat: - NSLocalizedString(@"BACKUP_EXPORT_COMPLETE_ALERT_MESSAGE_FORMAT", - @"Format for message for the 'backup export complete' alert. Embeds: {{the backup password}}."), - self.password]; - UIAlertController *alert = - [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", comment - : nil) - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *_Nonnull action) { - completion(); - }]; - [alert addAction:okAction]; - - UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; - [fromViewController presentViewController:alert animated:YES completion:nil]; -} - -- (void)showShareUI -{ - OWSAssertIsOnMainThread(); - OWSAssert(self.backupZipPath.length > 0); - - [AttachmentSharing showShareUIForURL:[NSURL fileURLWithPath:self.backupZipPath]]; + }); } - (void)exportToFilesAndZip @@ -173,21 +125,34 @@ NS_ASSUME_NONNULL_BEGIN [OWSFileSystem protectFolderAtPath:rootDirPath]; [OWSFileSystem ensureDirectoryExists:backupDirPath]; + if (self.isCancelled) { + return; + } + NSData *databasePassword = [TSStorageManager sharedManager].databasePassword; if (![self writeData:databasePassword fileName:@"databasePassword" backupDirPath:backupDirPath]) { return; } + if (self.isCancelled) { + return; + } if (![self writeUserDefaults:NSUserDefaults.standardUserDefaults fileName:@"standardUserDefaults" backupDirPath:backupDirPath]) { return; } + if (self.isCancelled) { + return; + } if (![self writeUserDefaults:NSUserDefaults.appUserDefaults fileName:@"appUserDefaults" backupDirPath:backupDirPath]) { return; } + if (self.isCancelled) { + return; + } // Use a read/write transaction to acquire a file lock on the database files. // // TODO: If we use multiple database files, lock them too. @@ -198,12 +163,18 @@ NS_ASSUME_NONNULL_BEGIN backupDirPath:backupDirPath]) { return; } + if (self.isCancelled) { + return; + } if (![self copyDirectory:OWSFileSystem.appSharedDataDirectoryPath dstDirName:@"appSharedDataDirectoryPath" backupDirPath:backupDirPath]) { return; } }]; + if (self.isCancelled) { + return; + } if (![self zipDirectory:backupDirPath dstFilePath:backupZipPath]) { return; } @@ -297,14 +268,13 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(srcDirPath.length > 0); OWSAssert(dstFilePath.length > 0); - OWSAssert(self.password.length > 0); BOOL success = [SSZipArchive createZipFileAtPath:dstFilePath withContentsOfDirectory:srcDirPath keepParentDirectory:NO compressionLevel:Z_DEFAULT_COMPRESSION - password:self.password - AES:YES + password:self.backupPassword + AES:self.backupPassword != nil progressHandler:^(NSUInteger entryNumber, NSUInteger total) { DDLogVerbose(@"%@ Zip progress: %zd / %zd = %f", self.logTag, diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 86faee4f0..414abd688 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -145,16 +145,34 @@ /* action sheet button title to enable built in speaker during a call */ "AUDIO_ROUTE_BUILT_IN_SPEAKER" = "Speaker"; -/* Format for message for the 'backup export complete' alert. Embeds: {{the backup password}} */ -"BACKUP_EXPORT_COMPLETE_ALERT_MESSAGE_FORMAT" = "Your backup password is: %@. Make sure to keep a copy of this password or you won't be able to restore from this backup."; +/* Message indicating that backup export without password is complete. */ +"BACKUP_EXPORT_COMPLETE_MESSAGE" = "Backup complete."; -/* Title for the 'backup export complete' alert. */ -"BACKUP_EXPORT_COMPLETE_ALERT_TITLE" = "Backup Complete"; +/* Label for button that copies backup password to the pasteboard. */ +"BACKUP_EXPORT_COPY_PASSWORD_BUTTON" = "Copy Password"; -/* Title for the 'backup export in progress' alert. */ -"BACKUP_EXPORT_IN_PROGRESS_ALERT_TITLE" = "Exporting Backup..."; +/* Message indicating that backup export is in progress. */ +"BACKUP_EXPORT_IN_PROGRESS_MESSAGE" = "Exporting Backup..."; -/* Format for backup filenames. Embeds: {{the date and time of the backup}} */ +/* Format for message indicating that backup export with password is complete. Embeds: {{the backup password}}. */ +"BACKUP_EXPORT_PASSWORD_MESSAGE_FORMAT" = "Your backup password is: %@. Make sure to keep a copy of this password or you won't be able to restore from this backup."; + +/* Label for button that 'send backup' in the current conversation. */ +"BACKUP_EXPORT_SEND_BACKUP_BUTTON" = "Send Backup as Message"; + +/* Message indicating that sending the backup failed. */ +"BACKUP_EXPORT_SEND_BACKUP_FAILED" = "Sending Backup Failed."; + +/* Message indicating that sending the backup succeeded. */ +"BACKUP_EXPORT_SEND_BACKUP_SUCCESS" = "Backup Sent."; + +/* Label for button that opens share UI for backup. */ +"BACKUP_EXPORT_SHARE_BACKUP_BUTTON" = "Share Backup"; + +/* Title for the 'backup export' view. */ +"BACKUP_EXPORT_VIEW_TITLE" = "Backup"; + +/* Format for backup filenames. Embeds: {{the date and time of the backup}}. Should not include characters like slash (/ or \\) or colon (:). */ "BACKUP_FILENAME_FORMAT" = "Signal Backup %@"; /* An explanation of the consequences of blocking another user. */ diff --git a/SignalMessaging/ViewControllers/OWSTableViewController.h b/SignalMessaging/ViewControllers/OWSTableViewController.h index b6c97efe0..e1c67e170 100644 --- a/SignalMessaging/ViewControllers/OWSTableViewController.h +++ b/SignalMessaging/ViewControllers/OWSTableViewController.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSViewController.h" @@ -50,10 +50,13 @@ typedef NS_ENUM(NSInteger, OWSTableItemType) { }; typedef void (^OWSTableActionBlock)(void); +typedef void (^OWSTableSubPageBlock)(UIViewController *viewController); typedef UITableViewCell *_Nonnull (^OWSTableCustomCellBlock)(void); @interface OWSTableItem : NSObject +@property (nonatomic, weak) UIViewController *tableViewController; + + (OWSTableItem *)itemWithTitle:(NSString *)title actionBlock:(nullable OWSTableActionBlock)actionBlock; + (OWSTableItem *)itemWithCustomCell:(UITableViewCell *)customCell @@ -73,6 +76,12 @@ typedef UITableViewCell *_Nonnull (^OWSTableCustomCellBlock)(void); customRowHeight:(CGFloat)customRowHeight actionBlock:(nullable OWSTableActionBlock)actionBlock; ++ (OWSTableItem *)subPageItemWithText:(NSString *)text actionBlock:(nullable OWSTableSubPageBlock)actionBlock; + ++ (OWSTableItem *)subPageItemWithText:(NSString *)text + customRowHeight:(CGFloat)customRowHeight + actionBlock:(nullable OWSTableSubPageBlock)actionBlock; + + (OWSTableItem *)actionItemWithText:(NSString *)text actionBlock:(nullable OWSTableActionBlock)actionBlock; + (OWSTableItem *)softCenterLabelItemWithText:(NSString *)text; diff --git a/SignalMessaging/ViewControllers/OWSTableViewController.m b/SignalMessaging/ViewControllers/OWSTableViewController.m index 063349288..157565956 100644 --- a/SignalMessaging/ViewControllers/OWSTableViewController.m +++ b/SignalMessaging/ViewControllers/OWSTableViewController.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSTableViewController.h" @@ -153,6 +153,7 @@ const CGFloat kOWSTable_DefaultCellHeight = 45.f; OWSTableItem *item = [OWSTableItem new]; item.itemType = OWSTableItemTypeAction; + __weak OWSTableItem *weakItem = item; item.actionBlock = actionBlock; item.customCellBlock = ^{ UITableViewCell *cell = [UITableViewCell new]; @@ -176,6 +177,45 @@ const CGFloat kOWSTable_DefaultCellHeight = 45.f; return item; } ++ (OWSTableItem *)subPageItemWithText:(NSString *)text actionBlock:(nullable OWSTableSubPageBlock)actionBlock +{ + OWSAssert(text.length > 0); + OWSAssert(actionBlock); + + OWSTableItem *item = [OWSTableItem new]; + item.itemType = OWSTableItemTypeAction; + __weak OWSTableItem *weakItem = item; + item.actionBlock = ^{ + OWSTableItem *strongItem = weakItem; + OWSAssert(strongItem); + OWSAssert(strongItem.tableViewController); + + if (actionBlock) { + actionBlock(strongItem.tableViewController); + } + }; + item.customCellBlock = ^{ + UITableViewCell *cell = [UITableViewCell new]; + cell.textLabel.text = text; + cell.textLabel.font = [UIFont ows_regularFontWithSize:18.f]; + cell.textLabel.textColor = [UIColor blackColor]; + cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; + return cell; + }; + return item; +} + ++ (OWSTableItem *)subPageItemWithText:(NSString *)text + customRowHeight:(CGFloat)customRowHeight + actionBlock:(nullable OWSTableSubPageBlock)actionBlock +{ + OWSAssert(customRowHeight > 0); + + OWSTableItem *item = [self subPageItemWithText:text actionBlock:actionBlock]; + item.customRowHeight = @(customRowHeight); + return item; +} + + (OWSTableItem *)actionItemWithText:(NSString *)text actionBlock:(nullable OWSTableActionBlock)actionBlock { OWSAssert(text.length > 0); @@ -481,6 +521,8 @@ NSString *const kOWSTableCellIdentifier = @"kOWSTableCellIdentifier"; { OWSTableItem *item = [self itemForIndexPath:indexPath]; + item.tableViewController = self; + UITableViewCell *customCell = [item customCell]; if (customCell) { return customCell; diff --git a/SignalMessaging/categories/UIView+OWS.h b/SignalMessaging/categories/UIView+OWS.h index d8287e78b..3951e219b 100644 --- a/SignalMessaging/categories/UIView+OWS.h +++ b/SignalMessaging/categories/UIView+OWS.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import @@ -95,9 +95,14 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value); - (NSTextAlignment)textAlignmentUnnatural; // Leading and trailing anchors honor layout margins. // When using a UIView as a "div" to structure layout, we don't want it to have margins. -+ (UIView *)containerView; - (void)setHLayoutMargins:(CGFloat)value; +#pragma mark - Containers + ++ (UIView *)containerView; + ++ (UIView *)verticalStackWithSubviews:(NSArray *)subviews spacing:(int)spacing; + #pragma mark - Debugging - (void)addBorderWithColor:(UIColor *)color; diff --git a/SignalMessaging/categories/UIView+OWS.m b/SignalMessaging/categories/UIView+OWS.m index ffbfbaa1a..f25a0eec0 100644 --- a/SignalMessaging/categories/UIView+OWS.m +++ b/SignalMessaging/categories/UIView+OWS.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSMath.h" @@ -388,6 +388,16 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) return (self.isRTL ? NSTextAlignmentLeft : NSTextAlignmentRight); } +- (void)setHLayoutMargins:(CGFloat)value +{ + UIEdgeInsets layoutMargins = self.layoutMargins; + layoutMargins.left = value; + layoutMargins.right = value; + self.layoutMargins = layoutMargins; +} + +#pragma mark - Containers + + (UIView *)containerView { UIView *view = [UIView new]; @@ -397,12 +407,22 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) return view; } -- (void)setHLayoutMargins:(CGFloat)value ++ (UIView *)verticalStackWithSubviews:(NSArray *)subviews spacing:(int)spacing { - UIEdgeInsets layoutMargins = self.layoutMargins; - layoutMargins.left = value; - layoutMargins.right = value; - self.layoutMargins = layoutMargins; + UIView *container = [UIView containerView]; + UIView *_Nullable lastSubview = nil; + for (UIView *subview in subviews) { + [container addSubview:subview]; + [subview autoPinWidthToSuperview]; + if (lastSubview) { + [subview autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:lastSubview withOffset:spacing]; + } else { + [subview autoPinEdgeToSuperviewEdge:ALEdgeTop]; + } + lastSubview = subview; + } + [lastSubview autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + return container; } #pragma mark - Debugging diff --git a/SignalServiceKit/src/Util/MIMETypeUtil.m b/SignalServiceKit/src/Util/MIMETypeUtil.m index 1f62471c3..cf26dd21f 100644 --- a/SignalServiceKit/src/Util/MIMETypeUtil.m +++ b/SignalServiceKit/src/Util/MIMETypeUtil.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "MIMETypeUtil.h" @@ -461,7 +461,6 @@ NSString *const kSyncMessageFileExtension = @"bin"; + (NSSet *)supportedVideoUTITypes { - OWSAssertIsOnMainThread(); static NSSet *result = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -472,7 +471,6 @@ NSString *const kSyncMessageFileExtension = @"bin"; + (NSSet *)supportedAudioUTITypes { - OWSAssertIsOnMainThread(); static NSSet *result = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -483,7 +481,6 @@ NSString *const kSyncMessageFileExtension = @"bin"; + (NSSet *)supportedImageUTITypes { - OWSAssertIsOnMainThread(); static NSSet *result = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -494,7 +491,6 @@ NSString *const kSyncMessageFileExtension = @"bin"; + (NSSet *)supportedAnimatedImageUTITypes { - OWSAssertIsOnMainThread(); static NSSet *result = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{