diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 2248d28931..890711cc23 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -53,6 +53,12 @@ static NSString *const kURLHostAddStickersPrefix = @"addstickers"; static NSTimeInterval launchStartedAt; +typedef NS_ENUM(NSUInteger, LaunchFailure) { + LaunchFailure_None, + LaunchFailure_CouldNotLoadDatabase, + LaunchFailure_UnknownDatabaseVersion, +}; + @interface AppDelegate () @property (nonatomic) BOOL hasInitialRootViewController; @@ -230,17 +236,21 @@ static NSTimeInterval launchStartedAt; // We need to do this _after_ we set up logging, when the keychain is unlocked, // but before we access YapDatabase, files on disk, or NSUserDefaults - NSError *error; - if (![YDBLegacyMigration ensureIsYDBReadyForAppExtensions:&error]) { - if (error != nil) { - [self showLaunchFailureUI:error]; - } + NSError *_Nullable launchError = nil; + LaunchFailure launchFailure = LaunchFailure_None; - // If this method has failed; do nothing. - // - // ensureIsReadyForAppExtensions will show a failure mode UI that - // lets users report this error. + BOOL isYdbNotReady = ![YDBLegacyMigration ensureIsYDBReadyForAppExtensions:&launchError]; + if (isYdbNotReady || launchError != nil) { + launchFailure = LaunchFailure_CouldNotLoadDatabase; + } else if (StorageCoordinator.hasInvalidDatabaseVersion) { + // Prevent: + // * Users who have used GRDB revert to using YDB. + // * Users with an unknown GRDB schema revert to using an earlier GRDB schema. + launchFailure = LaunchFailure_UnknownDatabaseVersion; + } + if (launchFailure != LaunchFailure_None) { OWSLogInfo(@"application: didFinishLaunchingWithOptions failed."); + [self showUIForLaunchFailure:launchFailure]; return YES; } @@ -374,7 +384,7 @@ static NSTimeInterval launchStartedAt; exit(0); } -- (void)showLaunchFailureUI:(NSError *)error +- (void)showUIForLaunchFailure:(LaunchFailure)launchFailure { // Disable normal functioning of app. self.didAppLaunchFail = YES; @@ -386,17 +396,36 @@ static NSTimeInterval launchStartedAt; self.window = [[OWSWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Show the launch screen - self.window.rootViewController = - [[UIStoryboard storyboardWithName:@"Launch Screen" bundle:nil] instantiateInitialViewController]; + UIViewController *viewController = [[UIStoryboard storyboardWithName:@"Launch Screen" + bundle:nil] instantiateInitialViewController]; + self.window.rootViewController = viewController; [self.window makeKeyAndVisible]; - UIAlertController *alert = - [UIAlertController alertControllerWithTitle:NSLocalizedString(@"APP_LAUNCH_FAILURE_ALERT_TITLE", - @"Title for the 'app launch failed' alert.") - message:NSLocalizedString(@"APP_LAUNCH_FAILURE_ALERT_MESSAGE", - @"Message for the 'app launch failed' alert.") - preferredStyle:UIAlertControllerStyleAlert]; + NSString *alertTitle; + NSString *alertMessage + = NSLocalizedString(@"APP_LAUNCH_FAILURE_ALERT_MESSAGE", @"Message for the 'app launch failed' alert."); + switch (launchFailure) { + case LaunchFailure_CouldNotLoadDatabase: + alertTitle = NSLocalizedString(@"APP_LAUNCH_FAILURE_COULD_NOT_LOAD_DATABASE", + @"Error indicating that the app could not launch because the database could not be loaded."); + break; + case LaunchFailure_UnknownDatabaseVersion: + alertTitle = NSLocalizedString(@"APP_LAUNCH_FAILURE_INVALID_DATABASE_VERSION_TITLE", + @"Error indicating that the app could not launch without reverting unknown database migrations."); + alertMessage = NSLocalizedString(@"APP_LAUNCH_FAILURE_INVALID_DATABASE_VERSION_MESSAGE", + @"Error indicating that the app could not launch without reverting unknown database migrations."); + break; + default: + OWSFailDebug(@"Unknown launch failure."); + alertTitle + = NSLocalizedString(@"APP_LAUNCH_FAILURE_ALERT_TITLE", @"Title for the 'app launch failed' alert."); + break; + } + + UIAlertController *alert = [UIAlertController alertControllerWithTitle:alertTitle + message:alertMessage + preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"SETTINGS_ADVANCED_SUBMIT_DEBUGLOG", nil) style:UIAlertActionStyleDefault @@ -405,8 +434,7 @@ static NSTimeInterval launchStartedAt; OWSFail(@"exiting after sharing debug logs."); }]; }]]; - UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; - [fromViewController presentAlert:alert]; + [viewController presentAlert:alert]; } - (void)startupLogging @@ -1024,6 +1052,10 @@ static NSTimeInterval launchStartedAt; - (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(nullable UIWindow *)window { + if (self.didAppLaunchFail) { + return UIInterfaceOrientationMaskPortrait; + } + if (self.hasCall) { OWSLogInfo(@"has call"); // The call-banner window is only suitable for portrait display diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index a93e1e25ed..5967fcb256 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -74,6 +74,15 @@ /* Title for the 'app launch failed' alert. */ "APP_LAUNCH_FAILURE_ALERT_TITLE" = "Error"; +/* Error indicating that the app could not launch because the database could not be loaded. */ +"APP_LAUNCH_FAILURE_COULD_NOT_LOAD_DATABASE" = "Could Not Load Database"; + +/* Error indicating that the app could not launch without reverting unknown database migrations. */ +"APP_LAUNCH_FAILURE_INVALID_DATABASE_VERSION_TITLE" = "Unknown Database Version."; + +/* Error indicating that the app could not launch without reverting unknown database migrations. */ +"APP_LAUNCH_FAILURE_INVALID_DATABASE_VERSION_MESSAGE" = "Please upgrade to the latest version of Signal."; + /* Text prompting user to edit their profile name. */ "APP_SETTINGS_EDIT_PROFILE_NAME_PROMPT" = "Enter your name"; diff --git a/SignalServiceKit/src/Storage/StorageCoordinator.h b/SignalServiceKit/src/Storage/StorageCoordinator.h index bbb3345105..c4e63825b6 100644 --- a/SignalServiceKit/src/Storage/StorageCoordinator.h +++ b/SignalServiceKit/src/Storage/StorageCoordinator.h @@ -38,6 +38,8 @@ NSString *NSStringFromStorageCoordinatorState(StorageCoordinatorState value); @property (class, nonatomic, readonly) BOOL hasYdbFile; @property (class, nonatomic, readonly) BOOL hasGrdbFile; +@property (class, nonatomic, readonly) BOOL hasInvalidDatabaseVersion; + - (BOOL)isDatabasePasswordAccessible; #ifdef TESTABLE_BUILD diff --git a/SignalServiceKit/src/Storage/StorageCoordinator.m b/SignalServiceKit/src/Storage/StorageCoordinator.m index 4a22686bc1..cc4491545d 100644 --- a/SignalServiceKit/src/Storage/StorageCoordinator.m +++ b/SignalServiceKit/src/Storage/StorageCoordinator.m @@ -71,6 +71,17 @@ NSString *NSStringFromStorageCoordinatorState(StorageCoordinatorState value) return [OWSFileSystem fileOrFolderExistsAtPath:grdbFilePath]; } ++ (BOOL)hasInvalidDatabaseVersion +{ + // A check to avoid trying to revert to YDB when we've already migrated to GRDB. + return (SSKFeatureFlags.storageMode == StorageModeYdb && self.hasGrdbFile && [SSKPreferences isYdbMigrated] && + // Allow developers to do this, but not QA, internal, + // public beta or production. + !SSKFeatureFlags.canRevertToYDB); + + // TODO: also return true if unknown GRDB version. +} + - (StorageCoordinatorState)storageCoordinatorState { return self.state; diff --git a/SignalServiceKit/src/Util/FeatureFlags.swift b/SignalServiceKit/src/Util/FeatureFlags.swift index 87a63d9692..e155f7ae39 100644 --- a/SignalServiceKit/src/Util/FeatureFlags.swift +++ b/SignalServiceKit/src/Util/FeatureFlags.swift @@ -106,6 +106,14 @@ public class FeatureFlags: NSObject { @objc public static let preserveYdb = true + @objc + public static var canRevertToYDB: Bool { + // Only developers should be allowed to use YDB after migrating to GRDB. + // We don't want to let QA, public beta or production users risk + // data loss. + return build == .dev + } + @objc public static var audibleErrorLogging = build.includes(.internalPreview)