Merge branch 'charlesmchen/backup'
This commit is contained in:
commit
76a295aaa5
1
Podfile
1
Podfile
@ -31,6 +31,7 @@ end
|
||||
target 'Signal' do
|
||||
shared_pods
|
||||
pod 'ATAppUpdater', :inhibit_warnings => true
|
||||
pod 'SSZipArchive', :inhibit_warnings => true
|
||||
|
||||
target 'SignalTests' do
|
||||
inherit! :search_paths
|
||||
|
||||
@ -59,6 +59,7 @@ PODS:
|
||||
- SQLCipher/common (3.4.1)
|
||||
- SQLCipher/standard (3.4.1):
|
||||
- SQLCipher/common
|
||||
- SSZipArchive (2.1.1)
|
||||
- TwistedOakCollapsingFutures (1.0.0):
|
||||
- UnionFind (~> 1.0)
|
||||
- UnionFind (1.0.1)
|
||||
@ -138,6 +139,7 @@ DEPENDENCIES:
|
||||
- Reachability
|
||||
- SignalServiceKit (from `.`)
|
||||
- SocketRocket (from `https://github.com/facebook/SocketRocket.git`)
|
||||
- SSZipArchive
|
||||
- YapDatabase/SQLCipher
|
||||
- YYImage
|
||||
|
||||
@ -200,11 +202,12 @@ SPEC CHECKSUMS:
|
||||
SignalServiceKit: 8f9038e584080bee8c367268067e6e0ec0feefcf
|
||||
SocketRocket: dbb1554b8fc288ef8ef370d6285aeca7361be31e
|
||||
SQLCipher: 43d12c0eb9c57fb438749618fc3ce0065509a559
|
||||
SSZipArchive: 14401ade5f8e82aba1ff03e9f88e9de60937ae60
|
||||
TwistedOakCollapsingFutures: f359b90f203e9ab13dfb92c9ff41842a7fe1cd0c
|
||||
UnionFind: c33be5adb12983981d6e827ea94fc7f9e370f52d
|
||||
YapDatabase: 299a32de9d350d37a9ac5b0532609d87d5d2a5de
|
||||
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
|
||||
|
||||
PODFILE CHECKSUM: bbc38aeceb39482a3cc46f0de44c6aef671e3e9f
|
||||
PODFILE CHECKSUM: fe273b089523b52e20652cebcf4854a80934f46c
|
||||
|
||||
COCOAPODS: 1.3.1
|
||||
|
||||
@ -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, ); }; };
|
||||
@ -117,8 +118,11 @@
|
||||
3478506C1FD9B78A007B8332 /* NoopNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347850681FD9B78A007B8332 /* NoopNotificationsManager.swift */; };
|
||||
347850711FDAEB17007B8332 /* OWSUserProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 3478506F1FDAEB16007B8332 /* OWSUserProfile.m */; };
|
||||
347850721FDAEB17007B8332 /* OWSUserProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = 347850701FDAEB16007B8332 /* OWSUserProfile.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
347E0B7A2003CD7500BC2F76 /* OWSBackupExportViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 347E0B762003CD7400BC2F76 /* OWSBackupExportViewController.m */; };
|
||||
347E0B7B2003CD7500BC2F76 /* OWSBackupImportViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 347E0B782003CD7400BC2F76 /* OWSBackupImportViewController.m */; };
|
||||
3497DBEC1ECE257500DB2605 /* OWSCountryMetadata.m in Sources */ = {isa = PBXBuildFile; fileRef = 3497DBEB1ECE257500DB2605 /* OWSCountryMetadata.m */; };
|
||||
3497DBEF1ECE2E4700DB2605 /* DomainFrontingCountryViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3497DBEE1ECE2E4700DB2605 /* DomainFrontingCountryViewController.m */; };
|
||||
34A910601FFEB114000C4745 /* OWSBackup.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A9105F1FFEB114000C4745 /* OWSBackup.m */; };
|
||||
34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B0796B1FCF46B000E248C2 /* MainAppContext.m */; };
|
||||
34B3F8711E8DF1700035BE1A /* AboutTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8351E8DF1700035BE1A /* AboutTableViewController.m */; };
|
||||
34B3F8721E8DF1700035BE1A /* AdvancedSettingsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8371E8DF1700035BE1A /* AdvancedSettingsTableViewController.m */; };
|
||||
@ -597,12 +601,18 @@
|
||||
347850681FD9B78A007B8332 /* NoopNotificationsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoopNotificationsManager.swift; sourceTree = "<group>"; };
|
||||
3478506F1FDAEB16007B8332 /* OWSUserProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSUserProfile.m; sourceTree = "<group>"; };
|
||||
347850701FDAEB16007B8332 /* OWSUserProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSUserProfile.h; sourceTree = "<group>"; };
|
||||
347E0B762003CD7400BC2F76 /* OWSBackupExportViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupExportViewController.m; sourceTree = "<group>"; };
|
||||
347E0B772003CD7400BC2F76 /* OWSBackupExportViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupExportViewController.h; sourceTree = "<group>"; };
|
||||
347E0B782003CD7400BC2F76 /* OWSBackupImportViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupImportViewController.m; sourceTree = "<group>"; };
|
||||
347E0B792003CD7500BC2F76 /* OWSBackupImportViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupImportViewController.h; sourceTree = "<group>"; };
|
||||
348F2EAD1F0D21BC00D4ECE0 /* DeviceSleepManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceSleepManager.swift; sourceTree = "<group>"; };
|
||||
3495BC911F1426B800B478F5 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = translations/ar.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3497DBEA1ECE257500DB2605 /* OWSCountryMetadata.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSCountryMetadata.h; sourceTree = "<group>"; };
|
||||
3497DBEB1ECE257500DB2605 /* OWSCountryMetadata.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSCountryMetadata.m; sourceTree = "<group>"; };
|
||||
3497DBED1ECE2E4700DB2605 /* DomainFrontingCountryViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DomainFrontingCountryViewController.h; sourceTree = "<group>"; };
|
||||
3497DBEE1ECE2E4700DB2605 /* DomainFrontingCountryViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DomainFrontingCountryViewController.m; sourceTree = "<group>"; };
|
||||
34A9105E1FFEB113000C4745 /* OWSBackup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackup.h; sourceTree = "<group>"; };
|
||||
34A9105F1FFEB114000C4745 /* OWSBackup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackup.m; sourceTree = "<group>"; };
|
||||
34B0796B1FCF46B000E248C2 /* MainAppContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MainAppContext.m; sourceTree = "<group>"; };
|
||||
34B0796C1FCF46B000E248C2 /* MainAppContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainAppContext.h; sourceTree = "<group>"; };
|
||||
34B0796E1FD07B1E00E248C2 /* SignalShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SignalShareExtension.entitlements; sourceTree = "<group>"; };
|
||||
@ -1320,6 +1330,10 @@
|
||||
34B3F8591E8DF1700035BE1A /* NotificationSettingsViewController.m */,
|
||||
34CCAF391F0C2748004084F4 /* OWSAddToContactViewController.h */,
|
||||
34CCAF3A1F0C2748004084F4 /* OWSAddToContactViewController.m */,
|
||||
347E0B772003CD7400BC2F76 /* OWSBackupExportViewController.h */,
|
||||
347E0B762003CD7400BC2F76 /* OWSBackupExportViewController.m */,
|
||||
347E0B792003CD7500BC2F76 /* OWSBackupImportViewController.h */,
|
||||
347E0B782003CD7400BC2F76 /* OWSBackupImportViewController.m */,
|
||||
34B3F85B1E8DF1700035BE1A /* OWSConversationSettingsViewController.h */,
|
||||
34B3F85C1E8DF1700035BE1A /* OWSConversationSettingsViewController.m */,
|
||||
34D5CCAB1EAE7136005515DB /* OWSConversationSettingsViewDelegate.h */,
|
||||
@ -1703,8 +1717,8 @@
|
||||
76EB04C818170B33006006FC /* util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B6DA6B061B8A2F9A00CA6F98 /* AppStoreRating.m */,
|
||||
B6DA6B051B8A2F9A00CA6F98 /* AppStoreRating.h */,
|
||||
B6DA6B061B8A2F9A00CA6F98 /* AppStoreRating.m */,
|
||||
34CCAF361F0C0599004084F4 /* AppUpdateNag.h */,
|
||||
34CCAF371F0C0599004084F4 /* AppUpdateNag.m */,
|
||||
B90418E4183E9DD40038554A /* DateUtil.h */,
|
||||
@ -1717,6 +1731,8 @@
|
||||
76EB04EC18170B33006006FC /* NumberUtil.h */,
|
||||
76EB04ED18170B33006006FC /* NumberUtil.m */,
|
||||
34D99C911F2937CC00D284D6 /* OWSAnalytics.swift */,
|
||||
34A9105E1FFEB113000C4745 /* OWSBackup.h */,
|
||||
34A9105F1FFEB114000C4745 /* OWSBackup.m */,
|
||||
4579431C1E7C8CE9008ED0C0 /* Pastelog.h */,
|
||||
4579431D1E7C8CE9008ED0C0 /* Pastelog.m */,
|
||||
450DF2041E0D74AC003D14BE /* Platform.swift */,
|
||||
@ -2527,6 +2543,7 @@
|
||||
"${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SAMKeychain/SAMKeychain.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SQLCipher/SQLCipher.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SSZipArchive/SSZipArchive.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SignalServiceKit/SignalServiceKit.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SocketRocket/SocketRocket.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/TwistedOakCollapsingFutures/TwistedOakCollapsingFutures.framework",
|
||||
@ -2552,6 +2569,7 @@
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SAMKeychain.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SQLCipher.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SSZipArchive.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SignalServiceKit.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SocketRocket.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/TwistedOakCollapsingFutures.framework",
|
||||
@ -2893,6 +2911,7 @@
|
||||
34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */,
|
||||
34B3F8801E8DF1700035BE1A /* InviteFlow.swift in Sources */,
|
||||
34B3F8871E8DF1700035BE1A /* NotificationSettingsViewController.m in Sources */,
|
||||
347E0B7B2003CD7500BC2F76 /* OWSBackupImportViewController.m in Sources */,
|
||||
458E38371D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m in Sources */,
|
||||
4517642B1DE939FD00EDB8B9 /* ContactCell.swift in Sources */,
|
||||
34B3F8751E8DF1700035BE1A /* CallViewController.swift in Sources */,
|
||||
@ -2927,6 +2946,7 @@
|
||||
FCC81A981A44558300DFEC7D /* UIDevice+TSHardwareVersion.m in Sources */,
|
||||
76EB054018170B33006006FC /* AppDelegate.m in Sources */,
|
||||
34D1F0831F8678AA0066283D /* ConversationInputTextView.m in Sources */,
|
||||
347E0B7A2003CD7500BC2F76 /* OWSBackupExportViewController.m in Sources */,
|
||||
34D1F0B11F867BFC0066283D /* OWSUnreadIndicatorCell.m in Sources */,
|
||||
34B3F89C1E8DF3270035BE1A /* BlockListViewController.m in Sources */,
|
||||
34BECE2B1F74C12700D7438D /* DebugUIStress.m in Sources */,
|
||||
@ -2945,6 +2965,7 @@
|
||||
34E8BF381EE9E2FD00F5F4CA /* FingerprintViewScanController.m in Sources */,
|
||||
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */,
|
||||
45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */,
|
||||
34A910601FFEB114000C4745 /* OWSBackup.m in Sources */,
|
||||
34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */,
|
||||
45A663C51F92EC760027B59E /* GroupTableViewCell.swift in Sources */,
|
||||
34D1F0861F8678AA0066283D /* ConversationViewController.m in Sources */,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AppDelegate.h"
|
||||
@ -9,6 +9,7 @@
|
||||
#import "DebugLogger.h"
|
||||
#import "MainAppContext.h"
|
||||
#import "NotificationsManager.h"
|
||||
#import "OWSBackup.h"
|
||||
#import "OWSNavigationController.h"
|
||||
#import "Pastelog.h"
|
||||
#import "PushManager.h"
|
||||
@ -132,6 +133,10 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
|
||||
// XXX - careful when moving this. It must happen before we initialize TSStorageManager.
|
||||
[self verifyDBKeysAvailableBeforeBackgroundLaunch];
|
||||
|
||||
// If a backup restore is in progress, try to complete it.
|
||||
// Otherwise, cleanup backup state.
|
||||
[OWSBackup applicationDidFinishLaunching];
|
||||
|
||||
// Prevent the device from sleeping during database view async registration
|
||||
// (e.g. long database upgrades).
|
||||
//
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AppSettingsViewController.h"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessageCell.h"
|
||||
@ -8,6 +8,8 @@
|
||||
#import "ConversationViewItem.h"
|
||||
#import "NSAttributedString+OWS.h"
|
||||
#import "OWSAudioMessageView.h"
|
||||
#import "OWSBackup.h"
|
||||
#import "OWSBackupImportViewController.h"
|
||||
#import "OWSExpirationTimerView.h"
|
||||
#import "OWSGenericAttachmentView.h"
|
||||
#import "Signal-Swift.h"
|
||||
@ -1314,7 +1316,16 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
[self.delegate didTapVideoViewItem:self.viewItem attachmentStream:self.attachmentStream];
|
||||
return;
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
#ifdef DEBUG
|
||||
if ([self.attachmentStream.filePath.lastPathComponent hasSuffix:OWSBackup_FileExtension]) {
|
||||
[self showBackupImportConfirmAlert:self.attachmentStream.filePath];
|
||||
} else {
|
||||
[AttachmentSharing showShareUIForAttachment:self.attachmentStream];
|
||||
}
|
||||
#else
|
||||
[AttachmentSharing showShareUIForAttachment:self.attachmentStream];
|
||||
#endif
|
||||
|
||||
break;
|
||||
case OWSMessageCellType_DownloadingAttachment: {
|
||||
OWSAssert(self.attachmentPointer);
|
||||
@ -1326,6 +1337,42 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showBackupImportConfirmAlert:(NSString *)backupZipPath
|
||||
{
|
||||
OWSAssert(backupZipPath.length > 0);
|
||||
|
||||
UIAlertController *controller =
|
||||
[UIAlertController alertControllerWithTitle:NSLocalizedString(@"BACKUP_IMPORT_CONFIRM_ALERT_TITLE",
|
||||
@"Title for alert confirming backup import.")
|
||||
message:NSLocalizedString(@"BACKUP_IMPORT_CONFIRM_ALERT_MESSAGE",
|
||||
@"Message for alert confirming backup import.")
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
[controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"BACKUP_IMPORT_CONFIRM_ALERT_BUTTON",
|
||||
@"Label for button confirming backup import.")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[self showBackupImportUI:backupZipPath];
|
||||
}]];
|
||||
[controller addAction:OWSAlerts.cancelAction];
|
||||
|
||||
UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController];
|
||||
[fromViewController presentViewController:controller animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)showBackupImportUI:(NSString *)backupZipPath
|
||||
{
|
||||
OWSAssert(backupZipPath.length > 0);
|
||||
|
||||
OWSBackupImportViewController *backupViewController = [OWSBackupImportViewController new];
|
||||
// TODO: Add support for restoring password-protected backups.
|
||||
[backupViewController importBackup:backupZipPath password:nil];
|
||||
UINavigationController *navigationController =
|
||||
[[UINavigationController alloc] initWithRootViewController:backupViewController];
|
||||
UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController];
|
||||
[fromViewController presentViewController:navigationController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)handleTextLongPressGesture:(UILongPressGestureRecognizer *)sender
|
||||
{
|
||||
OWSAssert(self.delegate);
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
//
|
||||
|
||||
#import "DebugUIMisc.h"
|
||||
#import "OWSBackup.h"
|
||||
#import "OWSBackupExportViewController.h"
|
||||
#import "OWSCountryMetadata.h"
|
||||
#import "OWSTableViewController.h"
|
||||
#import "RegistrationViewController.h"
|
||||
@ -93,6 +95,15 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
[DebugUIMisc sendUnencryptedDatabase:thread];
|
||||
}]];
|
||||
}
|
||||
[items addObject:[OWSTableItem
|
||||
subPageItemWithText:@"Export Backup w/o Password"
|
||||
actionBlock:^(UIViewController *viewController) {
|
||||
OWSBackupExportViewController *backupViewController =
|
||||
[OWSBackupExportViewController new];
|
||||
[backupViewController exportBackup:thread skipPassword:YES];
|
||||
[viewController.navigationController pushViewController:backupViewController
|
||||
animated:YES];
|
||||
}]];
|
||||
|
||||
return [OWSTableSection sectionWithTitle:self.name items:items];
|
||||
}
|
||||
|
||||
19
Signal/src/ViewControllers/OWSBackupExportViewController.h
Normal file
19
Signal/src/ViewControllers/OWSBackupExportViewController.h
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalMessaging/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSThread;
|
||||
|
||||
@interface OWSBackupExportViewController : 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
|
||||
312
Signal/src/ViewControllers/OWSBackupExportViewController.m
Normal file
312
Signal/src/ViewControllers/OWSBackupExportViewController.m
Normal file
@ -0,0 +1,312 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackupExportViewController.h"
|
||||
#import "OWSBackup.h"
|
||||
#import "OWSProgressView.h"
|
||||
#import "Signal-Swift.h"
|
||||
#import "ThreadUtil.h"
|
||||
#import <SignalMessaging/AttachmentSharing.h>
|
||||
#import <SignalMessaging/Environment.h>
|
||||
#import <SignalMessaging/SignalMessaging-Swift.h>
|
||||
#import <SignalMessaging/UIColor+OWS.h>
|
||||
#import <SignalMessaging/UIFont+OWS.h>
|
||||
#import <SignalMessaging/UIView+OWS.h>
|
||||
#import <SignalServiceKit/MIMETypeUtil.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSBackupExportViewController () <OWSBackupDelegate>
|
||||
|
||||
@property (nonatomic) OWSBackup *backup;
|
||||
|
||||
@property (nonatomic, nullable) OWSProgressView *progressView;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSBackupExportViewController
|
||||
|
||||
- (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];
|
||||
}
|
||||
self.progressView = nil;
|
||||
|
||||
switch (self.backup.backupState) {
|
||||
case OWSBackupState_InProgress:
|
||||
[self showInProgressUI];
|
||||
break;
|
||||
case OWSBackupState_Cancelled:
|
||||
[self showCancelledUI];
|
||||
break;
|
||||
case OWSBackupState_Complete:
|
||||
[self showCompleteUI];
|
||||
break;
|
||||
case OWSBackupState_Failed:
|
||||
[self showFailedUI];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showInProgressUI
|
||||
{
|
||||
self.progressView = [OWSProgressView new];
|
||||
self.progressView.color = [UIColor ows_materialBlueColor];
|
||||
self.progressView.progress = self.backup.backupProgress;
|
||||
[self.progressView autoSetDimension:ALDimensionWidth toSize:300];
|
||||
[self.progressView autoSetDimension:ALDimensionHeight toSize:20];
|
||||
|
||||
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];
|
||||
label.textAlignment = NSTextAlignmentCenter;
|
||||
label.numberOfLines = 0;
|
||||
label.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
|
||||
UIView *container = [UIView verticalStackWithSubviews:@[
|
||||
label,
|
||||
self.progressView,
|
||||
]
|
||||
spacing:10];
|
||||
[self.view addSubview:container];
|
||||
[container autoVCenterInSuperview];
|
||||
[container autoPinWidthToSuperviewWithMargin:25.f];
|
||||
}
|
||||
|
||||
- (void)showCancelledUI
|
||||
{
|
||||
// Show nothing.
|
||||
}
|
||||
|
||||
- (void)showCompleteUI
|
||||
{
|
||||
NSMutableArray<UIView *> *subviews = [NSMutableArray new];
|
||||
|
||||
{
|
||||
NSString *message = NSLocalizedString(
|
||||
@"BACKUP_EXPORT_COMPLETE_MESSAGE", @"Message indicating that backup export 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 "
|
||||
@"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];
|
||||
}
|
||||
|
||||
- (void)showFailedUI
|
||||
{
|
||||
NSMutableArray<UIView *> *subviews = [NSMutableArray new];
|
||||
|
||||
{
|
||||
NSString *message
|
||||
= NSLocalizedString(@"BACKUP_EXPORT_FAILED_MESSAGE", @"Message indicating that backup export failed.");
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// TODO: We could consider clearing the password from the pasteboard after a certain delay.
|
||||
[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];
|
||||
}
|
||||
|
||||
- (void)backupProgressDidChange
|
||||
{
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
self.progressView.progress = self.backup.backupProgress;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
17
Signal/src/ViewControllers/OWSBackupImportViewController.h
Normal file
17
Signal/src/ViewControllers/OWSBackupImportViewController.h
Normal file
@ -0,0 +1,17 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalMessaging/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSThread;
|
||||
|
||||
@interface OWSBackupImportViewController : OWSViewController
|
||||
|
||||
- (void)importBackup:(NSString *)backupZipPath password:(NSString *_Nullable)password;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
217
Signal/src/ViewControllers/OWSBackupImportViewController.m
Normal file
217
Signal/src/ViewControllers/OWSBackupImportViewController.m
Normal file
@ -0,0 +1,217 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackupImportViewController.h"
|
||||
#import "OWSBackup.h"
|
||||
#import "OWSProgressView.h"
|
||||
#import "Signal-Swift.h"
|
||||
#import "ThreadUtil.h"
|
||||
#import <SignalMessaging/AttachmentSharing.h>
|
||||
#import <SignalMessaging/Environment.h>
|
||||
#import <SignalMessaging/SignalMessaging-Swift.h>
|
||||
#import <SignalMessaging/UIColor+OWS.h>
|
||||
#import <SignalMessaging/UIFont+OWS.h>
|
||||
#import <SignalMessaging/UIView+OWS.h>
|
||||
#import <SignalServiceKit/MIMETypeUtil.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSBackupImportViewController () <OWSBackupDelegate>
|
||||
|
||||
@property (nonatomic) OWSBackup *backup;
|
||||
|
||||
@property (nonatomic, nullable) OWSProgressView *progressView;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSBackupImportViewController
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
self.view.backgroundColor = [UIColor whiteColor];
|
||||
|
||||
self.navigationItem.title = NSLocalizedString(@"BACKUP_IMPORT_VIEW_TITLE", @"Title for the 'backup import' view.");
|
||||
self.navigationItem.leftBarButtonItem =
|
||||
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop
|
||||
target:self
|
||||
action:@selector(dismissWasPressed:)];
|
||||
|
||||
self.backup.delegate = self;
|
||||
|
||||
[self updateUI];
|
||||
}
|
||||
|
||||
- (void)importBackup:(NSString *)backupZipPath password:(NSString *_Nullable)password
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssert(backupZipPath.length > 0);
|
||||
|
||||
// We set ourselves as the delegate of the backup later,
|
||||
// after we've loaded our view.
|
||||
self.backup = [OWSBackup new];
|
||||
[self.backup importBackup:backupZipPath password:password];
|
||||
}
|
||||
|
||||
- (void)updateUI
|
||||
{
|
||||
for (UIView *subview in self.view.subviews) {
|
||||
[subview removeFromSuperview];
|
||||
}
|
||||
self.progressView = nil;
|
||||
|
||||
switch (self.backup.backupState) {
|
||||
case OWSBackupState_InProgress:
|
||||
[self showInProgressUI];
|
||||
break;
|
||||
case OWSBackupState_Cancelled:
|
||||
[self showCancelledUI];
|
||||
break;
|
||||
case OWSBackupState_Complete:
|
||||
[self showCompleteUI];
|
||||
break;
|
||||
case OWSBackupState_Failed:
|
||||
[self showFailedUI];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showInProgressUI
|
||||
{
|
||||
self.progressView = [OWSProgressView new];
|
||||
self.progressView.color = [UIColor ows_materialBlueColor];
|
||||
self.progressView.progress = self.backup.backupProgress;
|
||||
[self.progressView autoSetDimension:ALDimensionWidth toSize:300];
|
||||
[self.progressView autoSetDimension:ALDimensionHeight toSize:20];
|
||||
|
||||
UILabel *label = [UILabel new];
|
||||
label.text = NSLocalizedString(
|
||||
@"BACKUP_IMPORT_IN_PROGRESS_MESSAGE", @"Message indicating that backup import is in progress.");
|
||||
label.textColor = [UIColor blackColor];
|
||||
label.font = [UIFont ows_regularFontWithSize:18.f];
|
||||
label.textAlignment = NSTextAlignmentCenter;
|
||||
label.numberOfLines = 0;
|
||||
label.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
|
||||
UIView *container = [UIView verticalStackWithSubviews:@[
|
||||
label,
|
||||
self.progressView,
|
||||
]
|
||||
spacing:10];
|
||||
[self.view addSubview:container];
|
||||
[container autoVCenterInSuperview];
|
||||
[container autoPinWidthToSuperviewWithMargin:25.f];
|
||||
}
|
||||
|
||||
- (void)showCancelledUI
|
||||
{
|
||||
// Show nothing.
|
||||
}
|
||||
|
||||
- (void)showCompleteUI
|
||||
{
|
||||
NSMutableArray<UIView *> *subviews = [NSMutableArray new];
|
||||
|
||||
{
|
||||
NSString *message = NSLocalizedString(
|
||||
@"BACKUP_IMPORT_COMPLETE_MESSAGE", @"Message indicating that backup import 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];
|
||||
}
|
||||
|
||||
[subviews addObject:[UIView new]];
|
||||
|
||||
[subviews addObject:[self makeButtonWithTitle:NSLocalizedString(@"BACKUP_IMPORT_RESTART_BUTTON",
|
||||
@"Label for button that restarts app to complete restore.")
|
||||
selector:@selector(restartApp)]];
|
||||
|
||||
UIView *container = [UIView verticalStackWithSubviews:subviews spacing:10];
|
||||
[self.view addSubview:container];
|
||||
[container autoVCenterInSuperview];
|
||||
[container autoPinWidthToSuperviewWithMargin:25.f];
|
||||
}
|
||||
|
||||
- (void)showFailedUI
|
||||
{
|
||||
NSMutableArray<UIView *> *subviews = [NSMutableArray new];
|
||||
|
||||
{
|
||||
NSString *message
|
||||
= NSLocalizedString(@"BACKUP_IMPORT_FAILED_MESSAGE", @"Message indicating that backup import failed.");
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
// 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)dismissWasPressed:(id)sender
|
||||
{
|
||||
[self.backup cancel];
|
||||
|
||||
[self.navigationController dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)restartApp
|
||||
{
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
[NSException raise:@"OWSBackup_RestartAppToCompleteBackupRestore" format:@"Killing app to complete backup restore"];
|
||||
}
|
||||
|
||||
#pragma mark - OWSBackupDelegate
|
||||
|
||||
- (void)backupStateDidChange
|
||||
{
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
[self updateUI];
|
||||
}
|
||||
|
||||
- (void)backupProgressDidChange
|
||||
{
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
self.progressView.progress = self.backup.backupProgress;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RegistrationViewController.h"
|
||||
@ -439,8 +439,9 @@ NSString *const kKeychainKey_LastRegisteredPhoneNumber = @"kKeychainKey_LastRegi
|
||||
OWSCAssert(value.length > 0);
|
||||
|
||||
NSError *error;
|
||||
[SAMKeychain setPassword:value forService:kKeychainService_LastRegistered account:key error:&error];
|
||||
if (error) {
|
||||
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
|
||||
BOOL success = [SAMKeychain setPassword:value forService:kKeychainService_LastRegistered account:key error:&error];
|
||||
if (!success || error) {
|
||||
DDLogError(@"%@ Error persisting 'last registered' value in keychain: %@", self.logTag, error);
|
||||
}
|
||||
}
|
||||
|
||||
75
Signal/src/util/OWSBackup.h
Normal file
75
Signal/src/util/OWSBackup.h
Normal file
@ -0,0 +1,75 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const OWSBackup_FileExtension;
|
||||
|
||||
@protocol OWSBackupDelegate <NSObject>
|
||||
|
||||
- (void)backupStateDidChange;
|
||||
|
||||
- (void)backupProgressDidChange;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
typedef NS_ENUM(NSUInteger, OWSBackupState) {
|
||||
OWSBackupState_InProgress,
|
||||
OWSBackupState_Cancelled,
|
||||
OWSBackupState_Complete,
|
||||
OWSBackupState_Failed,
|
||||
};
|
||||
|
||||
@class TSThread;
|
||||
|
||||
// We restore backups as part of the app launch process.
|
||||
//
|
||||
// applicationDidFinishLaunching must complete quickly even for
|
||||
// large backups, to prevent the app from being killed on launch.
|
||||
// Therefore, we break up backup import/restoration into two parts:
|
||||
//
|
||||
// * Preparation (which includes the costly decryption/unzip of the
|
||||
// backup file)
|
||||
// * Completion (file moves, NSUserDefaults writes, keychain writes).
|
||||
//
|
||||
// To protect data during backup and restore, we:
|
||||
//
|
||||
// * Optionally encrypt backup files with a password.
|
||||
// * Separately encrypt files containing keychain & NSUserDefaults data.
|
||||
// * Delete data from disk ASAP.
|
||||
@interface OWSBackup : NSObject
|
||||
|
||||
@property (nonatomic, weak) id<OWSBackupDelegate> delegate;
|
||||
|
||||
// An instance of `OWSBackup` is used for three separate tasks:
|
||||
//
|
||||
// * Backup export
|
||||
// * Backup import preparation
|
||||
// * Backup import completion
|
||||
//
|
||||
// The "backup state" and "progress" apply to all three tasks.
|
||||
@property (nonatomic, readonly) OWSBackupState backupState;
|
||||
@property (nonatomic, readonly) CGFloat backupProgress;
|
||||
|
||||
// If non-nil, backup is encrypted.
|
||||
@property (nonatomic, nullable, readonly) NSString *backupPassword;
|
||||
|
||||
// Only applies to "backup export" task.
|
||||
@property (nonatomic, nullable, readonly) TSThread *currentThread;
|
||||
|
||||
@property (nonatomic, readonly) NSString *backupZipPath;
|
||||
|
||||
- (void)exportBackup:(nullable TSThread *)currentThread skipPassword:(BOOL)skipPassword;
|
||||
|
||||
- (void)importBackup:(NSString *)backupZipPath password:(NSString *_Nullable)password;
|
||||
|
||||
- (void)cancel;
|
||||
|
||||
+ (void)applicationDidFinishLaunching;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
917
Signal/src/util/OWSBackup.m
Normal file
917
Signal/src/util/OWSBackup.m
Normal file
@ -0,0 +1,917 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackup.h"
|
||||
#import "NSUserDefaults+OWS.h"
|
||||
#import "Signal-Swift.h"
|
||||
#import "zlib.h"
|
||||
#import <SAMKeychain/SAMKeychain.h>
|
||||
#import <SSZipArchive/SSZipArchive.h>
|
||||
#import <SignalMessaging/SignalMessaging-Swift.h>
|
||||
#import <SignalServiceKit/Cryptography.h>
|
||||
#import <SignalServiceKit/OWSFileSystem.h>
|
||||
#import <SignalServiceKit/TSStorageManager.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// Hide the "import" directories from exports, etc. by prefixing their name with a period.
|
||||
NSString *const OWSBackup_DirNamePrefix = @".SignalBackup.";
|
||||
NSString *const OWSBackup_FileExtension = @".signalbackup";
|
||||
NSString *const OWSBackup_EncryptionKeyFilename = @".encryptionKey";
|
||||
NSString *const OWSBackup_DatabasePasswordFilename = @".databasePassword";
|
||||
NSString *const OWSBackup_StandardUserDefaultsFilename = @".standardUserDefaults";
|
||||
NSString *const OWSBackup_AppUserDefaultsFilename = @".appUserDefaults";
|
||||
NSString *const OWSBackup_AppDocumentDirName = @"appDocumentDirectoryPath";
|
||||
NSString *const OWSBackup_AppSharedDataDirName = @"appSharedDataDirectoryPath";
|
||||
|
||||
NSString *const NSUserDefaults_QueuedBackupPath = @"NSUserDefaults_QueuedBackupPath";
|
||||
|
||||
NSString *const Keychain_ImportBackupService = @"OWSKeychainService";
|
||||
NSString *const Keychain_ImportBackupKey = @"ImportBackupKey";
|
||||
|
||||
@interface OWSStorage (OWSBackup)
|
||||
|
||||
- (NSData *)databasePassword;
|
||||
|
||||
+ (void)storeDatabasePassword:(NSString *)password;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSBackup () <SSZipArchiveDelegate>
|
||||
|
||||
@property (nonatomic) OWSBackupState backupState;
|
||||
|
||||
@property (nonatomic) CGFloat backupProgress;
|
||||
|
||||
@property (nonatomic, nullable) TSThread *currentThread;
|
||||
|
||||
@property (nonatomic, nullable) NSString *backupPassword;
|
||||
|
||||
@property (nonatomic) NSString *backupDirPath;
|
||||
@property (nonatomic) NSString *backupZipPath;
|
||||
|
||||
@property (nonatomic) OWSAES256Key *encryptionKey;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSBackup
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
DDLogInfo(@"%@ Cleaning up: %@", self.logTag, self.backupDirPath);
|
||||
[OWSFileSystem deleteFileIfExists:self.backupDirPath];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[OWSBackup cleanupBackupState];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)setBackupState:(OWSBackupState)backupState
|
||||
{
|
||||
_backupState = backupState;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.delegate backupStateDidChange];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)setBackupProgress:(CGFloat)backupProgress
|
||||
{
|
||||
_backupProgress = backupProgress;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.delegate backupProgressDidChange];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)fail
|
||||
{
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
if (!self.isCancelledOrFailed) {
|
||||
self.backupState = OWSBackupState_Failed;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[OWSBackup cleanupBackupState];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)cancel
|
||||
{
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
if (!self.isCancelledOrFailed) {
|
||||
self.backupState = OWSBackupState_Cancelled;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[OWSBackup cleanupBackupState];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)complete
|
||||
{
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
if (!self.isCancelledOrFailed) {
|
||||
self.backupState = OWSBackupState_Complete;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isCancelledOrFailed
|
||||
{
|
||||
return (self.backupState == OWSBackupState_Cancelled || self.backupState == OWSBackupState_Failed);
|
||||
}
|
||||
|
||||
#pragma mark - Export Backup
|
||||
|
||||
- (void)exportBackup:(nullable TSThread *)currentThread skipPassword:(BOOL)skipPassword
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssert(CurrentAppContext().isMainApp);
|
||||
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
self.currentThread = currentThread;
|
||||
self.backupState = OWSBackupState_InProgress;
|
||||
|
||||
if (skipPassword) {
|
||||
DDLogInfo(@"%@ 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;
|
||||
DDLogInfo(@"%@ backup export with password: %@", self.logTag, backupPassword);
|
||||
}
|
||||
|
||||
[self startExport];
|
||||
}
|
||||
|
||||
- (void)startExport
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[self exportToFilesAndZip];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self complete];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void)exportToFilesAndZip
|
||||
{
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
// First, clean up any existing backup import/export state.
|
||||
[OWSBackup cleanupBackupState];
|
||||
|
||||
NSString *temporaryDirectory = NSTemporaryDirectory();
|
||||
NSString *rootDirName = [OWSBackup_DirNamePrefix stringByAppendingString:[NSUUID UUID].UUIDString];
|
||||
NSString *rootDirPath = [temporaryDirectory stringByAppendingPathComponent:rootDirName];
|
||||
NSString *backupDirPath = [rootDirPath stringByAppendingPathComponent:@"Contents"];
|
||||
|
||||
NSDateFormatter *dateFormatter = [NSDateFormatter new];
|
||||
[dateFormatter setLocale:[NSLocale currentLocale]];
|
||||
[dateFormatter setDateFormat:@"yyyy.MM.dd hh.mm.ss"];
|
||||
NSString *backupDateTime = [dateFormatter stringFromDate:[NSDate new]];
|
||||
NSString *backupName =
|
||||
[NSString stringWithFormat:NSLocalizedString(@"BACKUP_FILENAME_FORMAT",
|
||||
@"Format for backup filenames. Embeds: {{the date and time of the backup}}. "
|
||||
@"Should not include characters like slash (/ or \\) or colon (:)."),
|
||||
backupDateTime];
|
||||
NSString *backupZipPath =
|
||||
[rootDirPath stringByAppendingPathComponent:[backupName stringByAppendingString:OWSBackup_FileExtension]];
|
||||
self.backupDirPath = backupDirPath;
|
||||
self.backupZipPath = backupZipPath;
|
||||
DDLogInfo(@"%@ rootDirPath: %@", self.logTag, rootDirPath);
|
||||
DDLogInfo(@"%@ backupDirPath: %@", self.logTag, backupDirPath);
|
||||
DDLogInfo(@"%@ backupZipPath: %@", self.logTag, backupZipPath);
|
||||
|
||||
[OWSFileSystem ensureDirectoryExists:rootDirPath];
|
||||
[OWSFileSystem protectFileOrFolderAtPath:rootDirPath];
|
||||
[OWSFileSystem ensureDirectoryExists:backupDirPath];
|
||||
|
||||
if (self.isCancelledOrFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
OWSAES256Key *encryptionKey = [OWSAES256Key generateRandomKey];
|
||||
self.encryptionKey = encryptionKey;
|
||||
|
||||
NSData *databasePassword = [TSStorageManager sharedManager].databasePassword;
|
||||
|
||||
// TODO: We don't want this to reside unencrypted on disk even temporarily.
|
||||
// We need to encrypt this with a key that we hide in the keychain.
|
||||
if (![self writeData:databasePassword
|
||||
fileName:OWSBackup_DatabasePasswordFilename
|
||||
backupDirPath:backupDirPath
|
||||
encryptionKey:encryptionKey]) {
|
||||
return [self fail];
|
||||
}
|
||||
if (self.isCancelledOrFailed) {
|
||||
return;
|
||||
}
|
||||
if (![self writeUserDefaults:NSUserDefaults.standardUserDefaults
|
||||
fileName:OWSBackup_StandardUserDefaultsFilename
|
||||
backupDirPath:backupDirPath
|
||||
encryptionKey:encryptionKey]) {
|
||||
return [self fail];
|
||||
}
|
||||
if (self.isCancelledOrFailed) {
|
||||
return;
|
||||
}
|
||||
if (![self writeUserDefaults:NSUserDefaults.appUserDefaults
|
||||
fileName:OWSBackup_AppUserDefaultsFilename
|
||||
backupDirPath:backupDirPath
|
||||
encryptionKey:encryptionKey]) {
|
||||
return [self fail];
|
||||
}
|
||||
if (self.isCancelledOrFailed) {
|
||||
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.
|
||||
[TSStorageManager.sharedManager.newDatabaseConnection
|
||||
readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
||||
if (![self copyDirectory:OWSFileSystem.appDocumentDirectoryPath
|
||||
dstDirName:OWSBackup_AppDocumentDirName
|
||||
backupDirPath:backupDirPath]) {
|
||||
[self fail];
|
||||
return;
|
||||
}
|
||||
if (self.isCancelledOrFailed) {
|
||||
return;
|
||||
}
|
||||
if (![self copyDirectory:OWSFileSystem.appSharedDataDirectoryPath
|
||||
dstDirName:OWSBackup_AppSharedDataDirName
|
||||
backupDirPath:backupDirPath]) {
|
||||
[self fail];
|
||||
return;
|
||||
}
|
||||
}];
|
||||
if (self.isCancelledOrFailed) {
|
||||
return;
|
||||
}
|
||||
if (![self zipDirectory:backupDirPath dstFilePath:backupZipPath encryptionKey:encryptionKey]) {
|
||||
return [self fail];
|
||||
}
|
||||
|
||||
[OWSFileSystem protectFileOrFolderAtPath:backupZipPath];
|
||||
|
||||
[OWSFileSystem deleteFileIfExists:self.backupDirPath];
|
||||
}
|
||||
|
||||
- (BOOL)writeData:(NSData *)data
|
||||
fileName:(NSString *)fileName
|
||||
backupDirPath:(NSString *)backupDirPath
|
||||
encryptionKey:(OWSAES256Key *)encryptionKey
|
||||
{
|
||||
OWSAssert(data);
|
||||
OWSAssert(fileName.length > 0);
|
||||
OWSAssert(backupDirPath.length > 0);
|
||||
OWSAssert(encryptionKey);
|
||||
|
||||
NSData *_Nullable encryptedData = [Cryptography encryptAESGCMWithData:data key:encryptionKey];
|
||||
if (!encryptedData) {
|
||||
OWSFail(@"%@ failed to encrypt data: %@", self.logTag, fileName);
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSString *filePath = [backupDirPath stringByAppendingPathComponent:fileName];
|
||||
|
||||
DDLogInfo(@"%@ writeData: %@", self.logTag, filePath);
|
||||
|
||||
NSError *error;
|
||||
BOOL success = [encryptedData writeToFile:filePath options:NSDataWritingAtomic error:&error];
|
||||
if (!success || error) {
|
||||
OWSFail(@"%@ failed to write user defaults: %@", self.logTag, error);
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)copyDirectory:(NSString *)srcDirPath dstDirName:(NSString *)dstDirName backupDirPath:(NSString *)backupDirPath
|
||||
{
|
||||
OWSAssert(srcDirPath.length > 0);
|
||||
OWSAssert(dstDirName.length > 0);
|
||||
OWSAssert(backupDirPath.length > 0);
|
||||
|
||||
NSString *dstDirPath = [backupDirPath stringByAppendingPathComponent:dstDirName];
|
||||
|
||||
DDLogInfo(@"%@ copyDirectory: %@ -> %@", self.logTag, srcDirPath, dstDirPath);
|
||||
|
||||
// We "manually" copy the "root" items in the src directory.
|
||||
// Can't just use [NSFileManager copyItemAtPath:...] because the shared data container
|
||||
// contains files that the app is not allowed to access.
|
||||
[OWSFileSystem ensureDirectoryExists:dstDirPath];
|
||||
NSError *error = nil;
|
||||
NSArray<NSString *> *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:srcDirPath error:&error];
|
||||
if (error) {
|
||||
OWSFail(@"%@ failed to list directory: %@, %@", self.logTag, srcDirPath, error);
|
||||
return NO;
|
||||
}
|
||||
for (NSString *fileName in fileNames) {
|
||||
NSString *srcFilePath = [srcDirPath stringByAppendingPathComponent:fileName];
|
||||
NSString *dstFilePath = [dstDirPath stringByAppendingPathComponent:fileName];
|
||||
if ([fileName hasPrefix:@"."]) {
|
||||
DDLogInfo(@"%@ ignoring: %@", self.logTag, srcFilePath);
|
||||
continue;
|
||||
}
|
||||
BOOL success = [[NSFileManager defaultManager] copyItemAtPath:srcFilePath toPath:dstFilePath error:&error];
|
||||
if (!success || error) {
|
||||
OWSFail(@"%@ failed to copy directory item: %@, %@", self.logTag, srcFilePath, error);
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)writeUserDefaults:(NSUserDefaults *)userDefaults
|
||||
fileName:(NSString *)fileName
|
||||
backupDirPath:(NSString *)backupDirPath
|
||||
encryptionKey:(OWSAES256Key *)encryptionKey
|
||||
{
|
||||
OWSAssert(userDefaults);
|
||||
OWSAssert(fileName.length > 0);
|
||||
OWSAssert(backupDirPath.length > 0);
|
||||
OWSAssert(encryptionKey);
|
||||
|
||||
DDLogInfo(@"%@ writeUserDefaults: %@", self.logTag, fileName);
|
||||
|
||||
NSDictionary<NSString *, id> *dictionary = userDefaults.dictionaryRepresentation;
|
||||
if (!dictionary) {
|
||||
OWSFail(@"%@ failed to extract user defaults", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:dictionary];
|
||||
if (!data) {
|
||||
OWSFail(@"%@ failed to archive user defaults", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
|
||||
return [self writeData:data fileName:fileName backupDirPath:backupDirPath encryptionKey:encryptionKey];
|
||||
}
|
||||
|
||||
- (BOOL)zipDirectory:(NSString *)srcDirPath
|
||||
dstFilePath:(NSString *)dstFilePath
|
||||
encryptionKey:(OWSAES256Key *)encryptionKey
|
||||
{
|
||||
OWSAssert(srcDirPath.length > 0);
|
||||
OWSAssert(dstFilePath.length > 0);
|
||||
OWSAssert(encryptionKey);
|
||||
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
srcDirPath = [srcDirPath stringByStandardizingPath];
|
||||
OWSAssert(srcDirPath.length > 0);
|
||||
|
||||
NSError *error;
|
||||
NSArray<NSString *> *_Nullable srcFilePaths = [OWSFileSystem allFilesInDirectoryRecursive:srcDirPath error:&error];
|
||||
if (!srcFilePaths || error) {
|
||||
OWSFail(@"%@ failed to find files to zip: %@", self.logTag, error);
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Don't use the SSZipArchive convenience methods so that we can add the
|
||||
// encryption key directly as data.
|
||||
SSZipArchive *zipArchive = [[SSZipArchive alloc] initWithPath:dstFilePath];
|
||||
if (![zipArchive open]) {
|
||||
OWSFail(@"%@ failed to open zip file.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
for (NSString *srcFilePath in srcFilePaths) {
|
||||
NSString *relativePath = [self relativePathforPath:srcFilePath basePath:srcDirPath];
|
||||
BOOL success = [zipArchive writeFileAtPath:srcFilePath
|
||||
withFileName:relativePath
|
||||
compressionLevel:Z_DEFAULT_COMPRESSION
|
||||
password:self.backupPassword
|
||||
AES:self.backupPassword != nil];
|
||||
if (!success) {
|
||||
OWSFail(@"%@ failed to write file to zip file.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
// Write the encryption key directly into the zip so that it never
|
||||
// resides in plaintext on disk.
|
||||
BOOL success = [zipArchive writeData:encryptionKey.keyData
|
||||
filename:OWSBackup_EncryptionKeyFilename
|
||||
compressionLevel:Z_DEFAULT_COMPRESSION
|
||||
password:self.backupPassword
|
||||
AES:self.backupPassword != nil];
|
||||
if (!success) {
|
||||
OWSFail(@"%@ failed to write file to zip file.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
if (![zipArchive close]) {
|
||||
OWSFail(@"%@ failed to close zip file.", self.logTag);
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSNumber *fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:dstFilePath error:&error][NSFileSize];
|
||||
if (error) {
|
||||
OWSFail(@"%@ failed to get zip file size: %@", self.logTag, error);
|
||||
return NO;
|
||||
}
|
||||
DDLogInfo(@"%@ Zip file size: %@", self.logTag, fileSize);
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Import Backup, Part 1
|
||||
|
||||
- (void)importBackup:(NSString *)srcZipPath password:(NSString *_Nullable)password
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssert(srcZipPath.length > 0);
|
||||
OWSAssert(CurrentAppContext().isMainApp);
|
||||
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
self.backupPassword = password;
|
||||
|
||||
self.backupState = OWSBackupState_InProgress;
|
||||
|
||||
if (password.length == 0) {
|
||||
DDLogInfo(@"%@ backup import without password", self.logTag);
|
||||
} else {
|
||||
DDLogInfo(@"%@ backup import with password: %@", self.logTag, password);
|
||||
}
|
||||
|
||||
[self startImport:srcZipPath];
|
||||
}
|
||||
|
||||
- (void)startImport:(NSString *)srcZipPath
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssert(srcZipPath.length > 0);
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[self prepareForImport:srcZipPath];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self complete];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void)prepareForImport:(NSString *)srcZipPath
|
||||
{
|
||||
OWSAssert(srcZipPath.length > 0);
|
||||
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
NSString *documentDirectoryPath = OWSFileSystem.appDocumentDirectoryPath;
|
||||
NSString *rootDirName = [OWSBackup_DirNamePrefix stringByAppendingString:[NSUUID UUID].UUIDString];
|
||||
NSString *rootDirPath = [documentDirectoryPath stringByAppendingPathComponent:rootDirName];
|
||||
NSString *backupDirPath = [rootDirPath stringByAppendingPathComponent:@"Contents"];
|
||||
NSString *backupZipPath = [rootDirPath stringByAppendingPathComponent:srcZipPath.lastPathComponent];
|
||||
self.backupDirPath = backupDirPath;
|
||||
self.backupZipPath = backupZipPath;
|
||||
DDLogInfo(@"%@ rootDirPath: %@", self.logTag, rootDirPath);
|
||||
DDLogInfo(@"%@ backupDirPath: %@", self.logTag, backupDirPath);
|
||||
DDLogInfo(@"%@ backupZipPath: %@", self.logTag, backupZipPath);
|
||||
|
||||
[OWSFileSystem ensureDirectoryExists:rootDirPath];
|
||||
[OWSFileSystem protectFileOrFolderAtPath:rootDirPath];
|
||||
[OWSFileSystem ensureDirectoryExists:backupDirPath];
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success = [[NSFileManager defaultManager] copyItemAtPath:srcZipPath toPath:backupZipPath error:&error];
|
||||
if (!success || error) {
|
||||
OWSFail(@"%@ failed to copy backup zip: %@, %@", self.logTag, srcZipPath, error);
|
||||
return [self fail];
|
||||
}
|
||||
|
||||
if (self.isCancelledOrFailed) {
|
||||
return;
|
||||
}
|
||||
if (![self unzipFilePath]) {
|
||||
return [self fail];
|
||||
}
|
||||
if (self.isCancelledOrFailed) {
|
||||
return;
|
||||
}
|
||||
if (![self extractEncryptionKey]) {
|
||||
return [self fail];
|
||||
}
|
||||
if (self.isCancelledOrFailed) {
|
||||
return;
|
||||
}
|
||||
if (![self isValidBackup]) {
|
||||
return [self fail];
|
||||
}
|
||||
if (self.isCancelledOrFailed) {
|
||||
return;
|
||||
}
|
||||
if (![self enqueueBackupRestore]) {
|
||||
return [self fail];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)extractEncryptionKey
|
||||
{
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
NSString *encryptionKeyFilePath =
|
||||
[self.backupDirPath stringByAppendingPathComponent:OWSBackup_EncryptionKeyFilename];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:encryptionKeyFilePath]) {
|
||||
return NO;
|
||||
}
|
||||
NSData *_Nullable encryptionKeyData = [NSData dataWithContentsOfFile:encryptionKeyFilePath];
|
||||
if (!encryptionKeyData) {
|
||||
return NO;
|
||||
}
|
||||
OWSAES256Key *encryptionKey = [OWSAES256Key keyWithData:encryptionKeyData];
|
||||
if (!encryptionKey) {
|
||||
return NO;
|
||||
}
|
||||
self.encryptionKey = encryptionKey;
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:encryptionKeyFilePath error:&error];
|
||||
if (!success || error) {
|
||||
OWSFail(@"%@ could not delete encryption key file: %@", self.logTag, error);
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)unzipFilePath
|
||||
{
|
||||
OWSAssert(self.backupZipPath.length > 0);
|
||||
OWSAssert(self.backupDirPath.length > 0);
|
||||
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
// Don't use the SSZipArchive convenience methods so that we can add the
|
||||
// encryption key directly as data.
|
||||
|
||||
// TODO: Should we use preserveAttributes?
|
||||
NSError *error = nil;
|
||||
BOOL success = [SSZipArchive unzipFileAtPath:self.backupZipPath
|
||||
toDestination:self.backupDirPath
|
||||
preserveAttributes:YES
|
||||
overwrite:YES
|
||||
nestedZipLevel:0
|
||||
password:self.backupPassword
|
||||
error:&error
|
||||
delegate:self
|
||||
progressHandler:^(NSString *entry, unz_file_info zipInfo, long entryNumber, long total) {
|
||||
DDLogInfo(@"%@ progressHandler: %ld %ld", self.logTag, entryNumber, total);
|
||||
|
||||
CGFloat progress = entryNumber / (CGFloat)total;
|
||||
self.backupProgress = progress;
|
||||
}
|
||||
completionHandler:^(NSString *path, BOOL succeeded, NSError *_Nullable completionError) {
|
||||
DDLogInfo(@"%@ completionHandler: %d %@", self.logTag, succeeded, completionError);
|
||||
}];
|
||||
if (!success || error) {
|
||||
OWSFail(@"%@ failed to unzip file: %@.", self.logTag, error);
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)isValidBackup
|
||||
{
|
||||
NSString *databasePasswordFilePath =
|
||||
[self.backupDirPath stringByAppendingPathComponent:OWSBackup_DatabasePasswordFilename];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:databasePasswordFilePath]) {
|
||||
return NO;
|
||||
}
|
||||
NSString *standardUserDefaultsFilePath =
|
||||
[self.backupDirPath stringByAppendingPathComponent:OWSBackup_StandardUserDefaultsFilename];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:standardUserDefaultsFilePath]) {
|
||||
return NO;
|
||||
}
|
||||
NSString *appUserDefaultsFilePath =
|
||||
[self.backupDirPath stringByAppendingPathComponent:OWSBackup_AppUserDefaultsFilename];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:appUserDefaultsFilePath]) {
|
||||
return NO;
|
||||
}
|
||||
// TODO: Verify that the primary database exists.
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)enqueueBackupRestore
|
||||
{
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
NSError *error = nil;
|
||||
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
|
||||
BOOL success = [SAMKeychain setPasswordData:self.encryptionKey.keyData
|
||||
forService:Keychain_ImportBackupService
|
||||
account:Keychain_ImportBackupKey
|
||||
error:&error];
|
||||
if (!success || error) {
|
||||
OWSFail(@"%@ Could not store encryption key for import backup: %@", self.logTag, error);
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSString *documentDirectoryPath = OWSFileSystem.appDocumentDirectoryPath;
|
||||
NSString *relativePath = [self relativePathforPath:self.backupDirPath basePath:documentDirectoryPath];
|
||||
[[NSUserDefaults appUserDefaults] setObject:relativePath forKey:NSUserDefaults_QueuedBackupPath];
|
||||
[[NSUserDefaults appUserDefaults] synchronize];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Import Backup, Part 2
|
||||
|
||||
- (void)completeImportBackupIfPossible
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssert(CurrentAppContext().isMainApp);
|
||||
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
NSString *_Nullable queuedBackupRelativePath =
|
||||
[[NSUserDefaults appUserDefaults] stringForKey:NSUserDefaults_QueuedBackupPath];
|
||||
if (queuedBackupRelativePath.length == 0) {
|
||||
return;
|
||||
}
|
||||
NSString *documentDirectoryPath = OWSFileSystem.appDocumentDirectoryPath;
|
||||
NSString *_Nullable queuedBackupPath =
|
||||
[self joinRelativePath:queuedBackupRelativePath basePath:documentDirectoryPath];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:queuedBackupPath]) {
|
||||
OWSFail(@"%@ Missing import backup directory: %@.", self.logTag, queuedBackupPath);
|
||||
return;
|
||||
}
|
||||
self.backupDirPath = queuedBackupPath;
|
||||
self.backupState = OWSBackupState_InProgress;
|
||||
DDLogInfo(@"%@ queuedBackupPath: %@", self.logTag, queuedBackupPath);
|
||||
|
||||
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
|
||||
NSError *error;
|
||||
NSData *_Nullable encryptionKeyData =
|
||||
[SAMKeychain passwordDataForService:Keychain_ImportBackupService account:Keychain_ImportBackupKey error:&error];
|
||||
if (!encryptionKeyData || error) {
|
||||
OWSFail(@"%@ Could not retrieve encryption key for import backup: %@", self.logTag, error);
|
||||
return;
|
||||
}
|
||||
self.encryptionKey = [OWSAES256Key keyWithData:encryptionKeyData];
|
||||
|
||||
if (![self isValidBackup]) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *_Nullable databasePassword = [self readDataFromFileName:OWSBackup_DatabasePasswordFilename];
|
||||
if (!databasePassword) {
|
||||
OWSFail(@"%@ Could not retrieve database password.", self.logTag);
|
||||
return;
|
||||
}
|
||||
|
||||
// We can't restore a backup atomically, so we:
|
||||
//
|
||||
// * Ensure the restore consists only of tiny writes, and file moves.
|
||||
// * Write the database password last.
|
||||
if (![self loadUserDefaults:NSUserDefaults.standardUserDefaults fileName:OWSBackup_StandardUserDefaultsFilename]) {
|
||||
return;
|
||||
}
|
||||
if (![self loadUserDefaults:NSUserDefaults.appUserDefaults fileName:OWSBackup_AppUserDefaultsFilename]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (![self restoreDirectoryContents:OWSFileSystem.appDocumentDirectoryPath
|
||||
srcDirName:OWSBackup_AppDocumentDirName]) {
|
||||
return;
|
||||
}
|
||||
if (![self restoreDirectoryContents:OWSFileSystem.appSharedDataDirectoryPath
|
||||
srcDirName:OWSBackup_AppSharedDataDirName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Possibly verify database file location?
|
||||
|
||||
[OWSStorage storeDatabasePassword:[[NSString alloc] initWithData:databasePassword encoding:NSUTF8StringEncoding]];
|
||||
}
|
||||
|
||||
- (nullable NSData *)readDataFromFileName:(NSString *)fileName
|
||||
{
|
||||
OWSAssert(fileName.length > 0);
|
||||
OWSAssert(self.backupDirPath.length > 0);
|
||||
OWSAssert(self.encryptionKey);
|
||||
|
||||
NSString *filePath = [self.backupDirPath stringByAppendingPathComponent:fileName];
|
||||
|
||||
DDLogInfo(@"%@ readDataFromFileName: %@", self.logTag, filePath);
|
||||
|
||||
NSData *_Nullable encryptedData = [NSData dataWithContentsOfFile:filePath];
|
||||
if (!encryptedData) {
|
||||
OWSFail(@"%@ failed to read encrypted data: %@", self.logTag, fileName);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *_Nullable data = [Cryptography decryptAESGCMWithData:encryptedData key:self.encryptionKey];
|
||||
if (!data) {
|
||||
OWSFail(@"%@ failed to decrypt data: %@", self.logTag, fileName);
|
||||
return nil;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
- (BOOL)loadUserDefaults:(NSUserDefaults *)userDefaults fileName:(NSString *)fileName
|
||||
{
|
||||
OWSAssert(userDefaults);
|
||||
OWSAssert(fileName.length > 0);
|
||||
OWSAssert(self.backupDirPath.length > 0);
|
||||
OWSAssert(self.encryptionKey);
|
||||
|
||||
DDLogInfo(@"%@ loadUserDefaults: %@", self.logTag, fileName);
|
||||
|
||||
NSData *_Nullable data = [self readDataFromFileName:fileName];
|
||||
if (!data) {
|
||||
OWSFail(@"%@ Could not retrieve user defaults: %@.", self.logTag, fileName);
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
NSDictionary<NSString *, id> *_Nullable dictionary =
|
||||
[NSKeyedUnarchiver unarchiveTopLevelObjectWithData:data error:&error];
|
||||
if (!dictionary || error) {
|
||||
OWSFail(@"%@ Could not unarchive user defaults: %@", self.logTag, error);
|
||||
return NO;
|
||||
}
|
||||
if (![dictionary isKindOfClass:[NSDictionary class]]) {
|
||||
OWSFail(@"%@ Unexpected archived user defaults: %@", self.logTag, error);
|
||||
return NO;
|
||||
}
|
||||
|
||||
// TODO: this doesn't yet remove any keys, so you end up with the "union".
|
||||
for (NSString *key in dictionary) {
|
||||
id value = dictionary[key];
|
||||
OWSAssert(value);
|
||||
[userDefaults setObject:value forKey:key];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)restoreDirectoryContents:(NSString *)dstDirPath srcDirName:(NSString *)srcDirName
|
||||
{
|
||||
OWSAssert(srcDirName.length > 0);
|
||||
OWSAssert(dstDirPath.length > 0);
|
||||
OWSAssert(self.backupDirPath.length > 0);
|
||||
|
||||
NSString *srcDirPath = [self.backupDirPath stringByAppendingPathComponent:srcDirName];
|
||||
|
||||
DDLogInfo(@"%@ restoreDirectoryContents: %@ -> %@", self.logTag, srcDirPath, dstDirPath);
|
||||
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:srcDirPath]) {
|
||||
// Not all backups will have both a "app documents" and "shared data container" folder.
|
||||
// The latter should always be present for "modern" installs, but we are permissive
|
||||
// here about what we accept so that we can easily apply this branch to historic
|
||||
// (pre-shared data container) versions of the app and restore from them.
|
||||
DDLogInfo(@"%@ Skipping restore directory: %@.", self.logTag, srcDirPath);
|
||||
return YES;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
NSArray<NSString *> *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:srcDirPath error:&error];
|
||||
if (error) {
|
||||
OWSFail(@"%@ failed to list directory: %@, %@", self.logTag, srcDirPath, error);
|
||||
return NO;
|
||||
}
|
||||
for (NSString *fileName in fileNames) {
|
||||
NSString *srcFilePath = [srcDirPath stringByAppendingPathComponent:fileName];
|
||||
NSString *dstFilePath = [dstDirPath stringByAppendingPathComponent:fileName];
|
||||
|
||||
if ([[NSFileManager defaultManager] fileExistsAtPath:dstFilePath]) {
|
||||
// To replace an existing file or directory, rename the existing item
|
||||
// by adding a date/time suffix.
|
||||
NSDateFormatter *dateFormatter = [NSDateFormatter new];
|
||||
[dateFormatter setLocale:[NSLocale currentLocale]];
|
||||
[dateFormatter setDateFormat:@".yyyy.MM.dd hh.mm.ss"];
|
||||
NSString *replacementDateTime = [dateFormatter stringFromDate:[NSDate new]];
|
||||
|
||||
NSString *oldFilePath = [dstFilePath stringByAppendingString:replacementDateTime];
|
||||
BOOL success = [[NSFileManager defaultManager] moveItemAtPath:dstFilePath toPath:oldFilePath error:&error];
|
||||
if (!success || error) {
|
||||
OWSFail(@"%@ failed to move directory item: %@, %@", self.logTag, dstFilePath, error);
|
||||
return NO;
|
||||
}
|
||||
if (![OWSFileSystem protectFileOrFolderAtPath:oldFilePath]) {
|
||||
OWSFail(@"%@ failed to protect old directory item: %@, %@", self.logTag, oldFilePath, error);
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
BOOL success = [[NSFileManager defaultManager] moveItemAtPath:srcFilePath toPath:dstFilePath error:&error];
|
||||
if (!success || error) {
|
||||
OWSFail(@"%@ failed to move directory item: %@, %@", self.logTag, dstFilePath, error);
|
||||
return NO;
|
||||
}
|
||||
if (![OWSFileSystem protectFileOrFolderAtPath:dstFilePath]) {
|
||||
OWSFail(@"%@ failed to protect directory item: %@, %@", self.logTag, dstFilePath, error);
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Clean up
|
||||
|
||||
+ (void)cleanupBackupState
|
||||
{
|
||||
DDLogInfo(@"%@ %s.", self.logTag, __PRETTY_FUNCTION__);
|
||||
|
||||
[self cleanupBackupDirectoriesInDirectory:NSTemporaryDirectory()];
|
||||
[self cleanupBackupDirectoriesInDirectory:OWSFileSystem.appDocumentDirectoryPath];
|
||||
|
||||
[[NSUserDefaults appUserDefaults] removeObjectForKey:NSUserDefaults_QueuedBackupPath];
|
||||
[[NSUserDefaults appUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
+ (void)cleanupBackupDirectoriesInDirectory:(NSString *)dirPath
|
||||
{
|
||||
OWSAssert(dirPath.length > 0);
|
||||
|
||||
NSError *error;
|
||||
NSArray<NSString *> *filenames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:&error];
|
||||
if (error) {
|
||||
OWSFail(@"%@ could not find files in directory: %@", self.logTag, error);
|
||||
return;
|
||||
}
|
||||
|
||||
for (NSString *filename in filenames) {
|
||||
if (![filename hasPrefix:OWSBackup_DirNamePrefix]) {
|
||||
continue;
|
||||
}
|
||||
NSString *filePath = [dirPath stringByAppendingPathComponent:filename];
|
||||
BOOL success = [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
|
||||
if (!success || error) {
|
||||
OWSFail(@"%@ could not clean up backup directory: %@", self.logTag, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Utils
|
||||
|
||||
- (NSString *)relativePathforPath:(NSString *)filePath basePath:(NSString *)basePath
|
||||
{
|
||||
OWSAssert(filePath.stringByStandardizingPath.length > 0);
|
||||
OWSAssert([filePath.stringByStandardizingPath hasPrefix:basePath.stringByStandardizingPath]);
|
||||
|
||||
NSString *relativePath =
|
||||
[filePath.stringByStandardizingPath substringFromIndex:basePath.stringByStandardizingPath.length];
|
||||
NSString *separator = @"/";
|
||||
if ([relativePath hasPrefix:separator]) {
|
||||
relativePath = [relativePath substringFromIndex:separator.length];
|
||||
}
|
||||
OWSAssert(relativePath.length > 0);
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
- (NSString *)joinRelativePath:(NSString *)relativePath basePath:(NSString *)basePath
|
||||
{
|
||||
OWSAssert(basePath.stringByStandardizingPath.length > 0);
|
||||
OWSAssert(relativePath.length > 0);
|
||||
|
||||
return [basePath stringByAppendingPathComponent:relativePath];
|
||||
}
|
||||
|
||||
#pragma mark - App Launch
|
||||
|
||||
+ (void)applicationDidFinishLaunching
|
||||
{
|
||||
[[OWSBackup new] completeImportBackupIfPossible];
|
||||
|
||||
// Always clean up backup state on disk, but defer so as not to interface with
|
||||
// app launch sequence.
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
|
||||
[OWSBackup cleanupBackupState];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - SSZipArchiveDelegate
|
||||
|
||||
- (void)zipArchiveProgressEvent:(unsigned long long)loaded total:(unsigned long long)total
|
||||
{
|
||||
DDLogInfo(@"%@ zipArchiveProgressEvent: %llu %llu", self.logTag, loaded, total);
|
||||
|
||||
CGFloat progress = loaded / (CGFloat)total;
|
||||
self.backupProgress = progress;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -145,6 +145,63 @@
|
||||
/* action sheet button title to enable built in speaker during a call */
|
||||
"AUDIO_ROUTE_BUILT_IN_SPEAKER" = "Speaker";
|
||||
|
||||
/* Message indicating that backup export is complete. */
|
||||
"BACKUP_EXPORT_COMPLETE_MESSAGE" = "Backup complete.";
|
||||
|
||||
/* Label for button that copies backup password to the pasteboard. */
|
||||
"BACKUP_EXPORT_COPY_PASSWORD_BUTTON" = "Copy Password";
|
||||
|
||||
/* Message indicating that backup export failed. */
|
||||
"BACKUP_EXPORT_FAILED_MESSAGE" = "BACKUP_EXPORT_FAILED_MESSAGE";
|
||||
|
||||
/* Message indicating that backup export is in progress. */
|
||||
"BACKUP_EXPORT_IN_PROGRESS_MESSAGE" = "Exporting Backup...";
|
||||
|
||||
/* Format for message indicating that backup export 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 %@";
|
||||
|
||||
/* Message indicating that backup import is complete. */
|
||||
"BACKUP_IMPORT_COMPLETE_MESSAGE" = "Restore Complete";
|
||||
|
||||
/* Label for button confirming backup import. */
|
||||
"BACKUP_IMPORT_CONFIRM_ALERT_BUTTON" = "Restore";
|
||||
|
||||
/* Message for alert confirming backup import. */
|
||||
"BACKUP_IMPORT_CONFIRM_ALERT_MESSAGE" = "To restore this backup you must restart the Signal app. Warning: you will lose all of your current Signal data.";
|
||||
|
||||
/* Title for alert confirming backup import. */
|
||||
"BACKUP_IMPORT_CONFIRM_ALERT_TITLE" = "Restore Backup?";
|
||||
|
||||
/* Message indicating that backup import failed. */
|
||||
"BACKUP_IMPORT_FAILED_MESSAGE" = "Restore Failed";
|
||||
|
||||
/* Message indicating that backup import is in progress. */
|
||||
"BACKUP_IMPORT_IN_PROGRESS_MESSAGE" = "Restoring Backup...";
|
||||
|
||||
/* Label for button that restarts app to complete restore. */
|
||||
"BACKUP_IMPORT_RESTART_BUTTON" = "Restart App";
|
||||
|
||||
/* Title for the 'backup import' view. */
|
||||
"BACKUP_IMPORT_VIEW_TITLE" = "Restore Backup";
|
||||
|
||||
/* An explanation of the consequences of blocking another user. */
|
||||
"BLOCK_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages.";
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
@ -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<UIView *> *)subviews spacing:(int)spacing;
|
||||
|
||||
#pragma mark - Debugging
|
||||
|
||||
- (void)addBorderWithColor:(UIColor *)color;
|
||||
|
||||
@ -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<UIView *> *)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
|
||||
|
||||
@ -556,24 +556,11 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
|
||||
|
||||
- (NSString *)createAndSetNewDatabasePassword
|
||||
{
|
||||
NSString *newDBPassword = [[Randomness generateRandomBytes:30] base64EncodedString];
|
||||
NSError *keySetError;
|
||||
[SAMKeychain setPassword:newDBPassword forService:keychainService account:keychainDBPassAccount error:&keySetError];
|
||||
if (keySetError) {
|
||||
OWSProdCritical([OWSAnalyticsEvents storageErrorCouldNotStoreDatabasePassword]);
|
||||
NSString *password = [[Randomness generateRandomBytes:30] base64EncodedString];
|
||||
|
||||
[OWSStorage deletePasswordFromKeychain];
|
||||
[OWSStorage storeDatabasePassword:password];
|
||||
|
||||
// Sleep to give analytics events time to be delivered.
|
||||
[NSThread sleepForTimeInterval:15.0f];
|
||||
|
||||
[NSException raise:OWSStorageExceptionName_DatabasePasswordUnwritable
|
||||
format:@"Setting DB password failed with error: %@", keySetError];
|
||||
} else {
|
||||
DDLogWarn(@"Succesfully set new DB password.");
|
||||
}
|
||||
|
||||
return newDBPassword;
|
||||
return password;
|
||||
}
|
||||
|
||||
- (void)backgroundedAppDatabasePasswordInaccessibleWithErrorDescription:(NSString *)errorDescription
|
||||
@ -610,6 +597,27 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass";
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
+ (void)storeDatabasePassword:(NSString *)password
|
||||
{
|
||||
NSError *error;
|
||||
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
|
||||
BOOL success =
|
||||
[SAMKeychain setPassword:password forService:keychainService account:keychainDBPassAccount error:&error];
|
||||
if (!success || error) {
|
||||
OWSProdCritical([OWSAnalyticsEvents storageErrorCouldNotStoreDatabasePassword]);
|
||||
|
||||
[OWSStorage deletePasswordFromKeychain];
|
||||
|
||||
// Sleep to give analytics events time to be delivered.
|
||||
[NSThread sleepForTimeInterval:15.0f];
|
||||
|
||||
[NSException raise:OWSStorageExceptionName_DatabasePasswordUnwritable
|
||||
format:@"Setting DB password failed with error: %@", error];
|
||||
} else {
|
||||
DDLogWarn(@"Succesfully set new DB password.");
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@ -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<NSString *> *)supportedVideoUTITypes
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
static NSSet<NSString *> *result = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
@ -472,7 +471,6 @@ NSString *const kSyncMessageFileExtension = @"bin";
|
||||
|
||||
+ (NSSet<NSString *> *)supportedAudioUTITypes
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
static NSSet<NSString *> *result = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
@ -483,7 +481,6 @@ NSString *const kSyncMessageFileExtension = @"bin";
|
||||
|
||||
+ (NSSet<NSString *> *)supportedImageUTITypes
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
static NSSet<NSString *> *result = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
@ -494,7 +491,6 @@ NSString *const kSyncMessageFileExtension = @"bin";
|
||||
|
||||
+ (NSSet<NSString *> *)supportedAnimatedImageUTITypes
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
static NSSet<NSString *> *result = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
|
||||
@ -8,7 +8,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
+ (void)protectFileOrFolderAtPath:(NSString *)path;
|
||||
// TODO: We shouldn't ignore the return value of this method.
|
||||
+ (BOOL)protectFileOrFolderAtPath:(NSString *)path __attribute__((warn_unused_result));
|
||||
|
||||
+ (NSString *)appDocumentDirectoryPath;
|
||||
|
||||
@ -27,6 +28,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
+ (void)deleteFileIfExists:(NSString *)filePath;
|
||||
|
||||
+ (NSArray<NSString *> *_Nullable)allFilesInDirectoryRecursive:(NSString *)dirPath error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@ -9,10 +9,10 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation OWSFileSystem
|
||||
|
||||
+ (void)protectFileOrFolderAtPath:(NSString *)path
|
||||
+ (BOOL)protectFileOrFolderAtPath:(NSString *)path
|
||||
{
|
||||
if (![NSFileManager.defaultManager fileExistsAtPath:path]) {
|
||||
return;
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
@ -26,7 +26,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
if (error || !success) {
|
||||
OWSProdCritical([OWSAnalyticsEvents storageErrorFileProtection]);
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (NSString *)appDocumentDirectoryPath
|
||||
@ -131,6 +133,38 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSArray<NSString *> *_Nullable)allFilesInDirectoryRecursive:(NSString *)dirPath error:(NSError **)error
|
||||
{
|
||||
OWSAssert(dirPath.length > 0);
|
||||
|
||||
*error = nil;
|
||||
|
||||
NSArray<NSString *> *filenames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:error];
|
||||
if (*error) {
|
||||
OWSFail(@"%@ could not find files in directory: %@", self.logTag, *error);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray<NSString *> *filePaths = [NSMutableArray new];
|
||||
|
||||
for (NSString *filename in filenames) {
|
||||
NSString *filePath = [dirPath stringByAppendingPathComponent:filename];
|
||||
|
||||
BOOL isDirectory;
|
||||
[[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory];
|
||||
if (isDirectory) {
|
||||
[filePaths addObjectsFromArray:[self allFilesInDirectoryRecursive:filePath error:error]];
|
||||
if (*error) {
|
||||
return nil;
|
||||
}
|
||||
} else {
|
||||
[filePaths addObject:filePath];
|
||||
}
|
||||
}
|
||||
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
Loading…
Reference in New Issue
Block a user