From c7f5047056934ad7dacb53186725d43e71df9be0 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 27 Nov 2018 11:15:09 -0500 Subject: [PATCH] Handle iCloud status. --- .../OWSBackupSettingsViewController.m | 52 +++++++++++++++- .../ViewControllers/DebugUI/DebugUIBackup.m | 29 +++++---- Signal/src/util/Backup/OWSBackup.h | 2 + Signal/src/util/Backup/OWSBackup.m | 58 +++++++++++++++++ Signal/src/util/Backup/OWSBackupAPI.swift | 62 ++++++++++++++++--- Signal/src/util/Backup/OWSBackupExportJob.m | 25 ++++---- Signal/src/util/Backup/OWSBackupImportJob.m | 25 ++++---- .../translations/en.lproj/Localizable.strings | 15 +++-- .../ViewControllers/OWSTableViewController.h | 2 + .../ViewControllers/OWSTableViewController.m | 20 ++++++ 10 files changed, 240 insertions(+), 50 deletions(-) diff --git a/Signal/src/ViewControllers/AppSettings/OWSBackupSettingsViewController.m b/Signal/src/ViewControllers/AppSettings/OWSBackupSettingsViewController.m index a49910188..1f4b26a1b 100644 --- a/Signal/src/ViewControllers/AppSettings/OWSBackupSettingsViewController.m +++ b/Signal/src/ViewControllers/AppSettings/OWSBackupSettingsViewController.m @@ -6,6 +6,7 @@ #import "OWSBackup.h" #import "Signal-Swift.h" #import "ThreadUtil.h" +#import #import #import #import @@ -18,6 +19,8 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSBackupSettingsViewController () +@property (nonatomic, nullable) NSError *iCloudError; + @end #pragma mark - @@ -34,6 +37,10 @@ NS_ASSUME_NONNULL_BEGIN selector:@selector(backupStateDidChange:) name:NSNotificationNameBackupStateDidChange object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidBecomeActive:) + name:OWSApplicationDidBecomeActiveNotification + object:nil]; [self updateTableContents]; } @@ -46,7 +53,27 @@ NS_ASSUME_NONNULL_BEGIN - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; + [self updateTableContents]; + [self updateICloudStatus]; +} + +- (void)updateICloudStatus +{ + __weak OWSBackupSettingsViewController *weakSelf = self; + [[OWSBackupAPI checkCloudKitAccessObjc] + .then(^{ + dispatch_async(dispatch_get_main_queue(), ^{ + weakSelf.iCloudError = nil; + [weakSelf updateTableContents]; + }); + }) + .catch(^(NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + weakSelf.iCloudError = error; + [weakSelf updateTableContents]; + }); + }) retainUntilComplete]; } #pragma mark - Table Contents @@ -57,6 +84,20 @@ NS_ASSUME_NONNULL_BEGIN BOOL isBackupEnabled = [OWSBackup.sharedManager isBackupEnabled]; + if (self.iCloudError) { + OWSTableSection *iCloudSection = [OWSTableSection new]; + iCloudSection.headerTitle = NSLocalizedString( + @"SETTINGS_BACKUP_ICLOUD_STATUS", @"Label for iCloud status row in the in the backup settings view."); + [iCloudSection + addItem:[OWSTableItem + longDisclosureItemWithText:[OWSBackupAPI errorMessageForCloudKitAccessError:self.iCloudError] + actionBlock:^{ + [[UIApplication sharedApplication] + openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]]; + }]]; + [contents addSection:iCloudSection]; + } + // TODO: This UI is temporary. // Enabling backup will involve entering and registering a PIN. OWSTableSection *enableSection = [OWSTableSection new]; @@ -77,7 +118,7 @@ NS_ASSUME_NONNULL_BEGIN [progressSection addItem:[OWSTableItem labelItemWithText:NSLocalizedString(@"SETTINGS_BACKUP_STATUS", - @"Label for status row in the in the backup settings view.") + @"Label for backup status row in the in the backup settings view.") accessoryText:NSStringForBackupExportState(OWSBackup.sharedManager.backupExportState)]]; if (OWSBackup.sharedManager.backupExportState == OWSBackupState_InProgress) { if (OWSBackup.sharedManager.backupExportDescription) { @@ -141,9 +182,18 @@ NS_ASSUME_NONNULL_BEGIN - (void)backupStateDidChange:(NSNotification *)notification { + OWSAssertIsOnMainThread(); + [self updateTableContents]; } +- (void)applicationDidBecomeActive:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + [self updateICloudStatus]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIBackup.m b/Signal/src/ViewControllers/DebugUI/DebugUIBackup.m index 74e775cce..5ff520fa7 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIBackup.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIBackup.m @@ -6,6 +6,7 @@ #import "OWSBackup.h" #import "OWSTableViewController.h" #import "Signal-Swift.h" +#import #import @import CloudKit; @@ -80,18 +81,22 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertDebug(success); NSString *recipientId = self.tsAccountManager.localNumber; - [OWSBackupAPI checkCloudKitAccessWithCompletion:^(BOOL hasAccess) { - if (hasAccess) { - [OWSBackupAPI saveTestFileToCloudWithRecipientId:recipientId - fileUrl:[NSURL fileURLWithPath:filePath] - success:^(NSString *recordName) { - // Do nothing, the API method will log for us. - } - failure:^(NSError *error){ - // Do nothing, the API method will log for us. - }]; - } - }]; + [[OWSBackupAPI checkCloudKitAccessObjc] + .then(^{ + dispatch_async(dispatch_get_main_queue(), ^{ + [OWSBackupAPI saveTestFileToCloudWithRecipientId:recipientId + fileUrl:[NSURL fileURLWithPath:filePath] + success:^(NSString *recordName) { + // Do nothing, the API method will log for us. + } + failure:^(NSError *error){ + // Do nothing, the API method will log for us. + }]; + }); + }) + .catch(^(NSError *error){ + // Do nothing, the API method will log for us. + }) retainUntilComplete]; } + (void)checkForBackup diff --git a/Signal/src/util/Backup/OWSBackup.h b/Signal/src/util/Backup/OWSBackup.h index 6d243b3e8..214664d42 100644 --- a/Signal/src/util/Backup/OWSBackup.h +++ b/Signal/src/util/Backup/OWSBackup.h @@ -71,6 +71,8 @@ NSArray *MiscCollectionsToBackup(void); - (void)allRecipientIdsWithManifestsInCloud:(OWSBackupStringListBlock)success failure:(OWSBackupErrorBlock)failure; +- (void)checkCanExportBackup:(OWSBackupBoolBlock)success failure:(OWSBackupErrorBlock)failure; + - (void)checkCanImportBackup:(OWSBackupBoolBlock)success failure:(OWSBackupErrorBlock)failure; // TODO: After a successful import, we should enable backup and diff --git a/Signal/src/util/Backup/OWSBackup.m b/Signal/src/util/Backup/OWSBackup.m index ccce6fcb0..ef930bde0 100644 --- a/Signal/src/util/Backup/OWSBackup.m +++ b/Signal/src/util/Backup/OWSBackup.m @@ -7,10 +7,13 @@ #import "OWSBackupIO.h" #import "OWSBackupImportJob.h" #import "Signal-Swift.h" +#import #import #import #import +@import CloudKit; + NS_ASSUME_NONNULL_BEGIN NSString *const NSNotificationNameBackupStateDidChange = @"NSNotificationNameBackupStateDidChange"; @@ -131,6 +134,10 @@ NSArray *MiscCollectionsToBackup(void) selector:@selector(registrationStateDidChange) name:RegistrationStateDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(ckAccountChanged) + name:CKAccountChangedNotification + object:nil]; // We want to start a backup if necessary on app launch, but app launch is a // busy time and it's important to remain responsive, so wait a few seconds before @@ -403,6 +410,48 @@ NSArray *MiscCollectionsToBackup(void) }]; } +- (void)checkCanExportBackup:(OWSBackupBoolBlock)success failure:(OWSBackupErrorBlock)failure +{ + OWSAssertIsOnMainThread(); + + OWSLogInfo(@""); + + void (^failWithUnexpectedError)(void) = ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error = + [NSError errorWithDomain:OWSBackupErrorDomain + code:1 + userInfo:@{ + NSLocalizedDescriptionKey : NSLocalizedString(@"BACKUP_UNEXPECTED_ERROR", + @"Error shown when backup fails due to an unexpected error.") + }]; + failure(error); + }); + }; + + if (!self.tsAccountManager.isRegisteredAndReady) { + OWSLogError(@"Can't backup; not registered and ready."); + return failWithUnexpectedError(); + } + NSString *_Nullable recipientId = self.tsAccountManager.localNumber; + if (recipientId.length < 1) { + OWSFailDebug(@"Can't backup; missing recipientId."); + return failWithUnexpectedError(); + } + + [[OWSBackupAPI checkCloudKitAccessObjc] + .then(^{ + dispatch_async(dispatch_get_main_queue(), ^{ + success(YES); + }); + }) + .catch(^(NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + failure(error); + }); + }) retainUntilComplete]; +} + - (void)checkCanImportBackup:(OWSBackupBoolBlock)success failure:(OWSBackupErrorBlock)failure { OWSAssertIsOnMainThread(); @@ -502,6 +551,15 @@ NSArray *MiscCollectionsToBackup(void) [self postDidChangeNotification]; } +- (void)ckAccountChanged +{ + OWSAssertIsOnMainThread(); + + [self ensureBackupExportState]; + + [self postDidChangeNotification]; +} + #pragma mark - OWSBackupJobDelegate // We use a delegate method to avoid storing this key in memory. diff --git a/Signal/src/util/Backup/OWSBackupAPI.swift b/Signal/src/util/Backup/OWSBackupAPI.swift index 73531cd44..c1b2e4c22 100644 --- a/Signal/src/util/Backup/OWSBackupAPI.swift +++ b/Signal/src/util/Backup/OWSBackupAPI.swift @@ -5,6 +5,7 @@ import Foundation import SignalServiceKit import CloudKit +import PromiseKit // We don't worry about atomic writes. Each backup export // will diff against last successful backup. @@ -660,28 +661,71 @@ import CloudKit // MARK: - Access + @objc public enum BackupError: Int, Error { + case couldNotDetermineAccountStatus + case noAccount + case restrictedAccountStatus + } + @objc - public class func checkCloudKitAccess(completion: @escaping (Bool) -> Void) { + public class func checkCloudKitAccessObjc() -> AnyPromise { + return AnyPromise(checkCloudKitAccess()) + } + + public class func checkCloudKitAccess() -> Promise { + let (promise, resolver) = Promise.pending() CKContainer.default().accountStatus(completionHandler: { (accountStatus, error) in DispatchQueue.main.async { + if let error = error { + Logger.error("Unknown error: \(String(describing: error)).") + resolver.reject(error) + return + } switch accountStatus { case .couldNotDetermine: - Logger.error("could not determine CloudKit account status:\(String(describing: error)).") - OWSAlerts.showErrorAlert(message: NSLocalizedString("CLOUDKIT_STATUS_COULD_NOT_DETERMINE", comment: "Error indicating that the app could not determine that user's CloudKit account status")) - completion(false) + Logger.error("could not determine CloudKit account status: \(String(describing: error)).") + resolver.reject(BackupError.couldNotDetermineAccountStatus) case .noAccount: Logger.error("no CloudKit account.") - OWSAlerts.showErrorAlert(message: NSLocalizedString("CLOUDKIT_STATUS_NO_ACCOUNT", comment: "Error indicating that user does not have an iCloud account.")) - completion(false) + resolver.reject(BackupError.noAccount) case .restricted: Logger.error("restricted CloudKit account.") - OWSAlerts.showErrorAlert(message: NSLocalizedString("CLOUDKIT_STATUS_RESTRICTED", comment: "Error indicating that the app was prevented from accessing the user's CloudKit account.")) - completion(false) + resolver.reject(BackupError.restrictedAccountStatus) case .available: - completion(true) + Logger.verbose("CloudKit access okay.") + resolver.fulfill(()) } } }) + return promise + } + + @objc + public class func checkCloudKitAccessAndPresentAnyError() { + checkCloudKitAccess() + .catch({ (error) in + let errorMessage = self.errorMessage(forCloudKitAccessError: error) + OWSAlerts.showErrorAlert(message: errorMessage) + }) + .retainUntilComplete() + } + + @objc + public class func errorMessage(forCloudKitAccessError error: Error) -> String { + if let backupError = error as? BackupError { + Logger.error("Backup error: \(String(describing: backupError)).") + switch backupError { + case .couldNotDetermineAccountStatus: + return NSLocalizedString("CLOUDKIT_STATUS_COULD_NOT_DETERMINE", comment: "Error indicating that the app could not determine that user's iCloud account status") + case .noAccount: + return NSLocalizedString("CLOUDKIT_STATUS_NO_ACCOUNT", comment: "Error indicating that user does not have an iCloud account.") + case .restrictedAccountStatus: + return NSLocalizedString("CLOUDKIT_STATUS_RESTRICTED", comment: "Error indicating that the app was prevented from accessing the user's iCloud account.") + } + } else { + Logger.error("Unknown error: \(String(describing: error)).") + return NSLocalizedString("CLOUDKIT_STATUS_COULD_NOT_DETERMINE", comment: "Error indicating that the app could not determine that user's iCloud account status") + } } // MARK: - Retry diff --git a/Signal/src/util/Backup/OWSBackupExportJob.m b/Signal/src/util/Backup/OWSBackupExportJob.m index 2e1cf1dbc..33a3390b9 100644 --- a/Signal/src/util/Backup/OWSBackupExportJob.m +++ b/Signal/src/util/Backup/OWSBackupExportJob.m @@ -6,6 +6,7 @@ #import "OWSBackupIO.h" #import "OWSDatabaseMigration.h" #import "Signal-Swift.h" +#import #import #import #import @@ -340,17 +341,19 @@ NS_ASSUME_NONNULL_BEGIN [self updateProgressWithDescription:nil progress:nil]; __weak OWSBackupExportJob *weakSelf = self; - [OWSBackupAPI checkCloudKitAccessWithCompletion:^(BOOL hasAccess) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - if (hasAccess) { - [weakSelf start]; - } else { - [weakSelf failWithErrorDescription: - NSLocalizedString(@"BACKUP_EXPORT_ERROR_COULD_NOT_EXPORT", - @"Error indicating the backup export could not export the user's data.")]; - } - }); - }]; + [[OWSBackupAPI checkCloudKitAccessObjc] + .then(^{ + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf start]; + }); + }) + .catch(^(NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf failWithErrorDescription: + NSLocalizedString(@"BACKUP_EXPORT_ERROR_COULD_NOT_EXPORT", + @"Error indicating the backup export could not export the user's data.")]; + }); + }) retainUntilComplete]; } - (void)start diff --git a/Signal/src/util/Backup/OWSBackupImportJob.m b/Signal/src/util/Backup/OWSBackupImportJob.m index 5bc7333ca..ff9d2d57d 100644 --- a/Signal/src/util/Backup/OWSBackupImportJob.m +++ b/Signal/src/util/Backup/OWSBackupImportJob.m @@ -7,6 +7,7 @@ #import "OWSDatabaseMigration.h" #import "OWSDatabaseMigrationRunner.h" #import "Signal-Swift.h" +#import #import #import #import @@ -69,17 +70,19 @@ NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKe [self updateProgressWithDescription:nil progress:nil]; __weak OWSBackupImportJob *weakSelf = self; - [OWSBackupAPI checkCloudKitAccessWithCompletion:^(BOOL hasAccess) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - if (hasAccess) { - [weakSelf start]; - } else { - [weakSelf failWithErrorDescription: - NSLocalizedString(@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT", - @"Error indicating the backup import could not import the user's data.")]; - } - }); - }]; + [[OWSBackupAPI checkCloudKitAccessObjc] + .then(^{ + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf start]; + }); + }) + .catch(^(NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf failWithErrorDescription: + NSLocalizedString(@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT", + @"Error indicating the backup import could not import the user's data.")]; + }); + }) retainUntilComplete]; } - (void)start diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 1fbe80355..1eaa4fb75 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -410,14 +410,14 @@ /* The label for the 'restore backup' button. */ "CHECK_FOR_BACKUP_RESTORE" = "Restore"; -/* Error indicating that the app could not determine that user's CloudKit account status */ -"CLOUDKIT_STATUS_COULD_NOT_DETERMINE" = "There was an error communicating with iCloud for backups."; +/* Error indicating that the app could not determine that user's iCloud account status */ +"CLOUDKIT_STATUS_COULD_NOT_DETERMINE" = "Signal could not determine your iCloud account status. Sign in to your iCloud Account in the iOS settings app to backup your Signal data."; /* Error indicating that user does not have an iCloud account. */ -"CLOUDKIT_STATUS_NO_ACCOUNT" = "You do not have an iCloud Account for backups."; +"CLOUDKIT_STATUS_NO_ACCOUNT" = "No iCloud Account. Sign in to your iCloud Account in the iOS settings app to backup your Signal data."; -/* Error indicating that the app was prevented from accessing the user's CloudKit account. */ -"CLOUDKIT_STATUS_RESTRICTED" = "Signal was not allowed to access your iCloud account for backups."; +/* Error indicating that the app was prevented from accessing the user's iCloud account. */ +"CLOUDKIT_STATUS_RESTRICTED" = "Signal was denied access your iCloud account for backups. Grant Signal access to your iCloud Account in the iOS settings app to backup your Signal data."; /* The first of two messages demonstrating the chosen conversation color, by rendering this message in an outgoing message bubble. */ "COLOR_PICKER_DEMO_MESSAGE_1" = "Choose the color of outgoing messages in this conversation."; @@ -1964,6 +1964,9 @@ /* Label for switch in settings that controls whether or not backup is enabled. */ "SETTINGS_BACKUP_ENABLING_SWITCH" = "Backup Enabled"; +/* Label for iCloud status row in the in the backup settings view. */ +"SETTINGS_BACKUP_ICLOUD_STATUS" = "iCloud Status"; + /* Indicates that the last backup restore failed. */ "SETTINGS_BACKUP_IMPORT_STATUS_FAILED" = "Backup Restore Failed"; @@ -1982,7 +1985,7 @@ /* Label for phase row in the in the backup settings view. */ "SETTINGS_BACKUP_PROGRESS" = "Progress"; -/* Label for status row in the in the backup settings view. */ +/* Label for backup status row in the in the backup settings view. */ "SETTINGS_BACKUP_STATUS" = "Status"; /* Indicates that the last backup failed. */ diff --git a/SignalMessaging/ViewControllers/OWSTableViewController.h b/SignalMessaging/ViewControllers/OWSTableViewController.h index 0185f88b2..bbf69e576 100644 --- a/SignalMessaging/ViewControllers/OWSTableViewController.h +++ b/SignalMessaging/ViewControllers/OWSTableViewController.h @@ -101,6 +101,8 @@ typedef UITableViewCell *_Nonnull (^OWSTableCustomCellBlock)(void); + (OWSTableItem *)labelItemWithText:(NSString *)text accessoryText:(NSString *)accessoryText; ++ (OWSTableItem *)longDisclosureItemWithText:(NSString *)text actionBlock:(nullable OWSTableActionBlock)actionBlock; + + (OWSTableItem *)switchItemWithText:(NSString *)text isOn:(BOOL)isOn target:(id)target selector:(SEL)selector; + (OWSTableItem *)switchItemWithText:(NSString *)text diff --git a/SignalMessaging/ViewControllers/OWSTableViewController.m b/SignalMessaging/ViewControllers/OWSTableViewController.m index f4aeb07fb..99291855d 100644 --- a/SignalMessaging/ViewControllers/OWSTableViewController.m +++ b/SignalMessaging/ViewControllers/OWSTableViewController.m @@ -346,6 +346,26 @@ const CGFloat kOWSTable_DefaultCellHeight = 45.f; return item; } ++ (OWSTableItem *)longDisclosureItemWithText:(NSString *)text actionBlock:(nullable OWSTableActionBlock)actionBlock +{ + OWSAssertDebug(text.length > 0); + + OWSTableItem *item = [OWSTableItem new]; + item.customCellBlock = ^{ + UITableViewCell *cell = [OWSTableItem newCell]; + + cell.textLabel.text = text; + cell.textLabel.numberOfLines = 0; + cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping; + cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; + + return cell; + }; + item.customRowHeight = @(UITableViewAutomaticDimension); + item.actionBlock = actionBlock; + return item; +} + + (OWSTableItem *)switchItemWithText:(NSString *)text isOn:(BOOL)isOn target:(id)target selector:(SEL)selector { return [self switchItemWithText:text isOn:isOn isEnabled:YES target:target selector:selector];