Handle iCloud status.

pull/1/head
Matthew Chen 6 years ago
parent 3ad89e404d
commit c7f5047056

@ -6,6 +6,7 @@
#import "OWSBackup.h"
#import "Signal-Swift.h"
#import "ThreadUtil.h"
#import <PromiseKit/AnyPromise.h>
#import <SignalMessaging/AttachmentSharing.h>
#import <SignalMessaging/Environment.h>
#import <SignalMessaging/SignalMessaging-Swift.h>
@ -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

@ -6,6 +6,7 @@
#import "OWSBackup.h"
#import "OWSTableViewController.h"
#import "Signal-Swift.h"
#import <PromiseKit/AnyPromise.h>
#import <SignalCoreKit/Randomness.h>
@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

@ -71,6 +71,8 @@ NSArray<NSString *> *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

@ -7,10 +7,13 @@
#import "OWSBackupIO.h"
#import "OWSBackupImportJob.h"
#import "Signal-Swift.h"
#import <PromiseKit/AnyPromise.h>
#import <SignalCoreKit/Randomness.h>
#import <SignalServiceKit/OWSIdentityManager.h>
#import <SignalServiceKit/YapDatabaseConnection+OWS.h>
@import CloudKit;
NS_ASSUME_NONNULL_BEGIN
NSString *const NSNotificationNameBackupStateDidChange = @"NSNotificationNameBackupStateDidChange";
@ -131,6 +134,10 @@ NSArray<NSString *> *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<NSString *> *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<NSString *> *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.

@ -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<Void> {
let (promise, resolver) = Promise<Void>.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

@ -6,6 +6,7 @@
#import "OWSBackupIO.h"
#import "OWSDatabaseMigration.h"
#import "Signal-Swift.h"
#import <PromiseKit/AnyPromise.h>
#import <SignalCoreKit/NSData+OWS.h>
#import <SignalCoreKit/NSDate+OWS.h>
#import <SignalCoreKit/Threading.h>
@ -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

@ -7,6 +7,7 @@
#import "OWSDatabaseMigration.h"
#import "OWSDatabaseMigrationRunner.h"
#import "Signal-Swift.h"
#import <PromiseKit/AnyPromise.h>
#import <SignalCoreKit/NSData+OWS.h>
#import <SignalServiceKit/OWSBackgroundTask.h>
#import <SignalServiceKit/OWSFileSystem.h>
@ -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

@ -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. */

@ -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

@ -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];

Loading…
Cancel
Save