1625 lines
67 KiB
Swift
1625 lines
67 KiB
Swift
//
|
|
// Copyright 2022 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import CryptoKit
|
|
import GRDB
|
|
import Intents
|
|
import SignalServiceKit
|
|
import SignalUI
|
|
import WebRTC
|
|
|
|
enum LaunchPreflightError {
|
|
case unknownDatabaseVersion
|
|
case couldNotRestoreTransferredData
|
|
case databaseCorruptedAndMightBeRecoverable
|
|
case databaseUnrecoverablyCorrupted
|
|
case lastAppLaunchCrashed
|
|
case incrementalTSAttachmentMigrationFailed
|
|
case lowStorageSpaceAvailable
|
|
case possibleReadCorruptionCrashed
|
|
|
|
var supportTag: String {
|
|
switch self {
|
|
case .unknownDatabaseVersion:
|
|
return "LaunchFailure_UnknownDatabaseVersion"
|
|
case .couldNotRestoreTransferredData:
|
|
return "LaunchFailure_CouldNotRestoreTransferredData"
|
|
case .databaseCorruptedAndMightBeRecoverable:
|
|
return "LaunchFailure_DatabaseCorruptedAndMightBeRecoverable"
|
|
case .databaseUnrecoverablyCorrupted:
|
|
return "LaunchFailure_DatabaseUnrecoverablyCorrupted"
|
|
case .lastAppLaunchCrashed:
|
|
return "LaunchFailure_LastAppLaunchCrashed"
|
|
case .incrementalTSAttachmentMigrationFailed:
|
|
return "LaunchFailure_incrementalTSAttachmentMigrationFailed"
|
|
case .lowStorageSpaceAvailable:
|
|
return "LaunchFailure_NoDiskSpaceAvailable"
|
|
case .possibleReadCorruptionCrashed:
|
|
return "LaunchFailure_PossibleReadCorruption"
|
|
}
|
|
}
|
|
}
|
|
|
|
private func uncaughtExceptionHandler(_ exception: NSException) {
|
|
if DebugFlags.internalLogging {
|
|
Logger.error("exception: \(exception)")
|
|
Logger.error("name: \(exception.name)")
|
|
Logger.error("reason: \(String(describing: exception.reason))")
|
|
Logger.error("userInfo: \(String(describing: exception.userInfo))")
|
|
} else {
|
|
let reason = exception.reason ?? ""
|
|
let reasonData = reason.data(using: .utf8) ?? Data()
|
|
let reasonHash = Data(SHA256.hash(data: reasonData)).base64EncodedString()
|
|
|
|
var truncatedReason = reason.prefix(20)
|
|
if let spaceIndex = truncatedReason.lastIndex(of: " ") {
|
|
truncatedReason = truncatedReason[..<spaceIndex]
|
|
}
|
|
let maybeEllipsis = (truncatedReason.endIndex < reason.endIndex) ? "..." : ""
|
|
Logger.error("\(exception.name): \(truncatedReason)\(maybeEllipsis) (hash: \(reasonHash))")
|
|
}
|
|
Logger.error("callStackSymbols: \(exception.callStackSymbols.joined(separator: "\n"))")
|
|
Logger.flush()
|
|
}
|
|
|
|
@main
|
|
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
// MARK: - Constants
|
|
|
|
private enum Constants {
|
|
static let appLaunchesAttemptedKey = "AppLaunchesAttempted"
|
|
}
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
func applicationWillEnterForeground(_ application: UIApplication) {
|
|
Logger.info("")
|
|
}
|
|
|
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
|
AssertIsOnMainThread()
|
|
if CurrentAppContext().isRunningTests {
|
|
return
|
|
}
|
|
|
|
Logger.warn("")
|
|
|
|
if didAppLaunchFail {
|
|
return
|
|
}
|
|
|
|
appReadiness.runNowOrWhenAppDidBecomeReadySync { self.handleActivation() }
|
|
|
|
// Clear all notifications whenever we become active.
|
|
// When opening the app from a notification,
|
|
// AppDelegate.didReceiveLocalNotification will always
|
|
// be called _before_ we become active.
|
|
clearAppropriateNotificationsAndRestoreBadgeCount()
|
|
|
|
// On every activation, clear old temp directories.
|
|
ClearOldTemporaryDirectories()
|
|
|
|
// Ensure that all windows have the correct frame.
|
|
AppEnvironment.shared.windowManagerRef.updateWindowFrames()
|
|
}
|
|
|
|
private let flushQueue = DispatchQueue(label: "org.signal.flush", qos: .utility)
|
|
|
|
func applicationWillResignActive(_ application: UIApplication) {
|
|
AssertIsOnMainThread()
|
|
|
|
if didAppLaunchFail {
|
|
return
|
|
}
|
|
|
|
Logger.warn("")
|
|
|
|
clearAppropriateNotificationsAndRestoreBadgeCount()
|
|
|
|
let backgroundTask = OWSBackgroundTask(label: #function)
|
|
flushQueue.async {
|
|
defer { backgroundTask.end() }
|
|
Logger.flush()
|
|
}
|
|
}
|
|
|
|
func applicationDidEnterBackground(_ application: UIApplication) {
|
|
Logger.info("")
|
|
|
|
if shouldKillAppWhenBackgrounded {
|
|
Logger.flush()
|
|
exit(0)
|
|
}
|
|
}
|
|
|
|
func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
|
|
Logger.info("")
|
|
}
|
|
|
|
func applicationWillTerminate(_ application: UIApplication) {
|
|
Logger.info("")
|
|
Logger.flush()
|
|
}
|
|
|
|
// MARK: - App Launch
|
|
|
|
private lazy var appReadiness = AppReadinessImpl()
|
|
|
|
func application(
|
|
_ application: UIApplication,
|
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
|
) -> Bool {
|
|
let launchStartedAt = CACurrentMediaTime()
|
|
|
|
NSSetUncaughtExceptionHandler(uncaughtExceptionHandler(_:))
|
|
|
|
// This should be the first thing we do.
|
|
let mainAppContext = MainAppContext()
|
|
SetCurrentAppContext(mainAppContext)
|
|
|
|
let debugLogger = DebugLogger.shared
|
|
debugLogger.enableTTYLoggingIfNeeded()
|
|
DebugLogger.registerLibsignal()
|
|
DebugLogger.registerRingRTC()
|
|
|
|
if mainAppContext.isRunningTests {
|
|
_ = initializeWindow(mainAppContext: mainAppContext, rootViewController: UIViewController())
|
|
return true
|
|
}
|
|
|
|
debugLogger.enableFileLogging(appContext: mainAppContext, canLaunchInBackground: true)
|
|
DebugLogger.configureSwiftLogging()
|
|
if DebugFlags.audibleErrorLogging {
|
|
debugLogger.enableErrorReporting()
|
|
}
|
|
|
|
Logger.warn("Launching…")
|
|
defer { Logger.info("Launched.") }
|
|
|
|
BenchEventStart(title: "Presenting HomeView", eventId: "AppStart", logInProduction: true)
|
|
appReadiness.runNowOrWhenUIDidBecomeReadySync { BenchEventComplete(eventId: "AppStart") }
|
|
|
|
MessageFetchBGRefreshTask.register(appReadiness: appReadiness)
|
|
|
|
let keychainStorage = KeychainStorageImpl(isUsingProductionService: TSConstants.isUsingProductionService)
|
|
let deviceTransferService = DeviceTransferService(
|
|
appReadiness: appReadiness,
|
|
keychainStorage: keychainStorage
|
|
)
|
|
|
|
AppEnvironment.setSharedEnvironment(AppEnvironment(
|
|
appReadiness: appReadiness,
|
|
deviceTransferService: deviceTransferService
|
|
))
|
|
|
|
// This *must* happen before we try and access or verify the database,
|
|
// since we may be in a state where the database has been partially
|
|
// restored from transfer (e.g. the key was replaced, but the database
|
|
// files haven't been moved into place)
|
|
let didDeviceTransferRestoreSucceed = Bench(
|
|
title: "Slow device transfer service launch",
|
|
logIfLongerThan: 0.01,
|
|
logInProduction: true,
|
|
block: { deviceTransferService.launchCleanup() }
|
|
)
|
|
|
|
let databaseStorage: SDSDatabaseStorage
|
|
do {
|
|
databaseStorage = try SDSDatabaseStorage(
|
|
appReadiness: appReadiness,
|
|
databaseFileUrl: SDSDatabaseStorage.grdbDatabaseFileUrl,
|
|
keychainStorage: keychainStorage
|
|
)
|
|
} catch KeychainError.notAllowed where application.applicationState == .background {
|
|
notifyThatPhoneMustBeUnlocked()
|
|
} catch {
|
|
// It's so corrupt that we can't even try to repair it.
|
|
didAppLaunchFail = true
|
|
Logger.error("Couldn't launch with broken database: \(error.grdbErrorForLogging)")
|
|
let viewController = terminalErrorViewController()
|
|
_ = initializeWindow(mainAppContext: mainAppContext, rootViewController: viewController)
|
|
presentDatabaseUnrecoverablyCorruptedError(from: viewController, action: .submitDebugLogsAndCrash)
|
|
return true
|
|
}
|
|
|
|
// This must happen in appDidFinishLaunching or earlier to ensure we don't
|
|
// miss notifications. Setting the delegate also seems to prevent us from
|
|
// getting the legacy notification notification callbacks upon launch e.g.
|
|
// 'didReceiveLocalNotification'
|
|
UNUserNotificationCenter.current().delegate = self
|
|
|
|
// If there's a notification, queue it up for processing. (This processing
|
|
// may happen immediately, after a short delay, or never.)
|
|
if let remoteNotification = launchOptions?[.remoteNotification] as? [AnyHashable: Any] {
|
|
Logger.info("Application was launched by tapping a push notification.")
|
|
processRemoteNotification(remoteNotification, completion: {})
|
|
}
|
|
|
|
// Do this even if `appVersion` isn't used -- there's side effects.
|
|
let appVersion = AppVersionImpl.shared
|
|
|
|
// Set up and register incremental migration for TSAttachment -> v2 Attachment.
|
|
// TODO: remove this (and the incremental migrator itself) once we make this
|
|
// migration a launch-blocking GRDB migration.
|
|
let incrementalMessageTSAttachmentMigrationStore = IncrementalTSAttachmentMigrationStore(
|
|
userDefaults: mainAppContext.appUserDefaults()
|
|
)
|
|
let incrementalMessageTSAttachmentMigratorFactory = IncrementalMessageTSAttachmentMigratorFactoryImpl(
|
|
store: incrementalMessageTSAttachmentMigrationStore
|
|
)
|
|
|
|
let launchContext = LaunchContext(
|
|
appContext: mainAppContext,
|
|
databaseStorage: databaseStorage,
|
|
keychainStorage: keychainStorage,
|
|
launchStartedAt: launchStartedAt,
|
|
incrementalMessageTSAttachmentMigrationStore: incrementalMessageTSAttachmentMigrationStore,
|
|
incrementalMessageTSAttachmentMigratorFactory: incrementalMessageTSAttachmentMigratorFactory
|
|
)
|
|
|
|
// We need to do this _after_ we set up logging, when the keychain is unlocked,
|
|
// but before we access the database or files on disk.
|
|
let preflightError = checkIfAllowedToLaunch(
|
|
mainAppContext: mainAppContext,
|
|
appVersion: appVersion,
|
|
incrementalTSAttachmentMigrationStore: incrementalMessageTSAttachmentMigrationStore,
|
|
didDeviceTransferRestoreSucceed: didDeviceTransferRestoreSucceed
|
|
)
|
|
|
|
if let preflightError {
|
|
didAppLaunchFail = true
|
|
let viewController = terminalErrorViewController()
|
|
let window = initializeWindow(mainAppContext: mainAppContext, rootViewController: viewController)
|
|
showPreflightErrorUI(
|
|
preflightError,
|
|
launchContext: launchContext,
|
|
window: window,
|
|
viewController: viewController
|
|
)
|
|
return true
|
|
}
|
|
|
|
// If this is a regular launch, increment the "launches attempted" counter.
|
|
// If repeatedly start launching but never finish them (ie the app is
|
|
// crashing while launching), we'll notice in `checkIfAllowedToLaunch`.
|
|
let userDefaults = mainAppContext.appUserDefaults()
|
|
let appLaunchesAttempted = userDefaults.integer(forKey: Constants.appLaunchesAttemptedKey)
|
|
userDefaults.set(appLaunchesAttempted + 1, forKey: Constants.appLaunchesAttemptedKey)
|
|
|
|
// We _must_ register BGProcessingTask handlers synchronously in didFinishLaunching.
|
|
// https://developer.apple.com/documentation/backgroundtasks/bgtaskscheduler/register(fortaskwithidentifier:using:launchhandler:)
|
|
// WARNING: Apple docs say we can only have 10 BGProcessingTasks registered.
|
|
IncrementalMessageTSAttachmentMigrationRunner.registerBGProcessingTask(
|
|
store: incrementalMessageTSAttachmentMigrationStore,
|
|
migrator: Task {
|
|
await withCheckedContinuation { continuation in
|
|
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
|
|
continuation.resume(with: .success(
|
|
DependenciesBridge.shared.incrementalMessageTSAttachmentMigrator
|
|
))
|
|
}
|
|
}
|
|
},
|
|
db: databaseStorage
|
|
)
|
|
let attachmentBackfillStore = AttachmentValidationBackfillStore()
|
|
AttachmentValidationBackfillRunner.registerBGProcessingTask(
|
|
store: attachmentBackfillStore,
|
|
migrator: Task {
|
|
await withCheckedContinuation { continuation in
|
|
appReadiness.runNowOrWhenMainAppDidBecomeReadyAsync {
|
|
continuation.resume(with: .success(
|
|
DependenciesBridge.shared.attachmentValidationBackfillMigrator
|
|
))
|
|
}
|
|
}
|
|
},
|
|
db: databaseStorage
|
|
)
|
|
|
|
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
|
|
if SSKEnvironment.shared.remoteConfigManagerRef.currentConfig().shouldRunTSAttachmentMigrationInBGProcessingTask {
|
|
IncrementalMessageTSAttachmentMigrationRunner.scheduleBGProcessingTaskIfNeeded(
|
|
store: incrementalMessageTSAttachmentMigrationStore,
|
|
db: databaseStorage
|
|
)
|
|
}
|
|
AttachmentValidationBackfillRunner.scheduleBGProcessingTaskIfNeeded(
|
|
store: attachmentBackfillStore,
|
|
db: databaseStorage
|
|
)
|
|
}
|
|
|
|
// Show LoadingViewController until the database migrations are complete.
|
|
let loadingViewController = LoadingViewController()
|
|
|
|
let window = initializeWindow(mainAppContext: mainAppContext, rootViewController: loadingViewController)
|
|
self.launchApp(in: window, launchContext: launchContext)
|
|
return true
|
|
}
|
|
|
|
var window: UIWindow?
|
|
|
|
private func initializeWindow(mainAppContext: MainAppContext, rootViewController: UIViewController) -> UIWindow {
|
|
let window = OWSWindow()
|
|
self.window = window
|
|
mainAppContext.mainWindow = window
|
|
window.rootViewController = rootViewController
|
|
window.makeKeyAndVisible()
|
|
return window
|
|
}
|
|
|
|
private struct LaunchContext {
|
|
var appContext: MainAppContext
|
|
var databaseStorage: SDSDatabaseStorage
|
|
var keychainStorage: any KeychainStorage
|
|
var launchStartedAt: CFTimeInterval
|
|
var incrementalMessageTSAttachmentMigrationStore: IncrementalTSAttachmentMigrationStore
|
|
var incrementalMessageTSAttachmentMigratorFactory: IncrementalMessageTSAttachmentMigratorFactory
|
|
}
|
|
|
|
private func launchApp(
|
|
in window: UIWindow,
|
|
launchContext: LaunchContext
|
|
) {
|
|
assert(window.rootViewController is LoadingViewController)
|
|
configureGlobalUI(in: window)
|
|
setUpMainAppEnvironment(
|
|
launchContext: launchContext
|
|
).done(on: DispatchQueue.main) { (finalContinuation, sleepBlockObject) in
|
|
self.didLoadDatabase(
|
|
finalContinuation: finalContinuation,
|
|
launchContext: launchContext,
|
|
sleepBlockObject: sleepBlockObject,
|
|
window: window
|
|
)
|
|
}
|
|
}
|
|
|
|
private lazy var screenLockUI = ScreenLockUI(appReadiness: appReadiness)
|
|
|
|
private func configureGlobalUI(in window: UIWindow) {
|
|
Theme.setupSignalAppearance()
|
|
|
|
screenLockUI.setupWithRootWindow(window)
|
|
AppEnvironment.shared.windowManagerRef.setupWithRootWindow(window, screenBlockingWindow: screenLockUI.screenBlockingWindow)
|
|
screenLockUI.startObserving()
|
|
}
|
|
|
|
private func setUpMainAppEnvironment(
|
|
launchContext: LaunchContext
|
|
) -> Guarantee<(AppSetup.FinalContinuation, DeviceSleepManager.BlockObject)> {
|
|
let sleepBlockObject = DeviceSleepManager.BlockObject(blockReason: "app launch")
|
|
DeviceSleepManager.shared.addBlock(blockObject: sleepBlockObject)
|
|
|
|
let _currentCall = AtomicValue<SignalCall?>(nil, lock: .init())
|
|
let currentCall = CurrentCall(rawValue: _currentCall)
|
|
|
|
let databaseContinuation = AppSetup().start(
|
|
appContext: launchContext.appContext,
|
|
appReadiness: appReadiness,
|
|
databaseStorage: launchContext.databaseStorage,
|
|
paymentsEvents: PaymentsEventsMainApp(),
|
|
mobileCoinHelper: MobileCoinHelperSDK(),
|
|
callMessageHandler: WebRTCCallMessageHandler(),
|
|
currentCallProvider: currentCall,
|
|
notificationPresenter: NotificationPresenterImpl(),
|
|
incrementalMessageTSAttachmentMigratorFactory: launchContext.incrementalMessageTSAttachmentMigratorFactory,
|
|
messageBackupErrorPresenterFactory: MessageBackupErrorPresenterFactoryInternal()
|
|
)
|
|
setupNSEInteroperation()
|
|
SUIEnvironment.shared.setUp(
|
|
appReadiness: appReadiness,
|
|
authCredentialManager: databaseContinuation.authCredentialManager
|
|
)
|
|
AppEnvironment.shared.setUp(
|
|
appReadiness: appReadiness,
|
|
callService: CallService(
|
|
appContext: launchContext.appContext,
|
|
appReadiness: appReadiness,
|
|
authCredentialManager: databaseContinuation.authCredentialManager,
|
|
callLinkPublicParams: databaseContinuation.callLinkPublicParams,
|
|
callLinkStore: DependenciesBridge.shared.callLinkStore,
|
|
callRecordDeleteManager: DependenciesBridge.shared.callRecordDeleteManager,
|
|
callRecordStore: DependenciesBridge.shared.callRecordStore,
|
|
db: DependenciesBridge.shared.db,
|
|
mutableCurrentCall: _currentCall,
|
|
networkManager: SSKEnvironment.shared.networkManagerRef,
|
|
tsAccountManager: DependenciesBridge.shared.tsAccountManager
|
|
)
|
|
)
|
|
let result = databaseContinuation.prepareDatabase()
|
|
return result.map(on: SyncScheduler()) { ($0, sleepBlockObject) }
|
|
}
|
|
|
|
private func checkSomeDiskSpaceAvailable() -> Bool {
|
|
let tempDir = URL(fileURLWithPath: NSTemporaryDirectory())
|
|
.appendingPathComponent(UUID().uuidString)
|
|
.path
|
|
let succeededCreatingDir = OWSFileSystem.ensureDirectoryExists(tempDir)
|
|
|
|
// Best effort at deleting temp dir, which shouldn't ever fail
|
|
if succeededCreatingDir && !OWSFileSystem.deleteFile(tempDir) {
|
|
owsFailDebug("Failed to delete temp dir used for checking disk space!")
|
|
}
|
|
|
|
return succeededCreatingDir
|
|
}
|
|
|
|
private func setupNSEInteroperation() {
|
|
// We immediately post a notification letting the NSE know the main app has launched.
|
|
// If it's running it should take this as a sign to terminate so we don't unintentionally
|
|
// try and fetch messages from two processes at once.
|
|
DarwinNotificationCenter.postNotification(name: .mainAppLaunched)
|
|
|
|
let appReadiness: AppReadiness = self.appReadiness
|
|
|
|
// We listen to this notification for the lifetime of the application, so we don't
|
|
// record the returned observer token.
|
|
_ = DarwinNotificationCenter.addObserver(
|
|
name: .nseDidReceiveNotification,
|
|
queue: DispatchQueue.global(qos: .userInitiated)
|
|
) { token in
|
|
// Immediately let the NSE know we will handle this notification so that it
|
|
// does not attempt to process messages while we are active.
|
|
DarwinNotificationCenter.postNotification(name: .mainAppHandledNotification)
|
|
|
|
appReadiness.runNowOrWhenAppDidBecomeReadySync {
|
|
_ = SSKEnvironment.shared.messageFetcherJobRef.run()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func didLoadDatabase(
|
|
finalContinuation: AppSetup.FinalContinuation,
|
|
launchContext: LaunchContext,
|
|
sleepBlockObject: DeviceSleepManager.BlockObject,
|
|
window: UIWindow
|
|
) {
|
|
AssertIsOnMainThread()
|
|
|
|
// First thing; clean up any transfer state in case we are launching after a transfer.
|
|
// This needs to happen before we check any registration state.
|
|
DependenciesBridge.shared.registrationStateChangeManager.cleanUpTransferStateOnAppLaunchIfNeeded()
|
|
|
|
let regLoader = RegistrationCoordinatorLoaderImpl(dependencies: .from(self))
|
|
|
|
// Before we mark ready, block message processing on any pending change numbers.
|
|
let hasPendingChangeNumber = SSKEnvironment.shared.databaseStorageRef.read { transaction in
|
|
regLoader.hasPendingChangeNumber(transaction: transaction.asV2Read)
|
|
}
|
|
if hasPendingChangeNumber {
|
|
// The registration loader will clear the suspension later on.
|
|
SSKEnvironment.shared.messagePipelineSupervisorRef.suspendMessageProcessingWithoutHandle(for: .pendingChangeNumber)
|
|
}
|
|
|
|
let launchInterface = buildLaunchInterface(regLoader: regLoader)
|
|
|
|
let hasInProgressRegistration: Bool
|
|
switch launchInterface {
|
|
case .registration, .secondaryProvisioning:
|
|
hasInProgressRegistration = true
|
|
case .chatList:
|
|
hasInProgressRegistration = false
|
|
}
|
|
|
|
switch finalContinuation.finish(willResumeInProgressRegistration: hasInProgressRegistration) {
|
|
case .corruptRegistrationState:
|
|
let viewController = terminalErrorViewController()
|
|
window.rootViewController = viewController
|
|
presentLaunchFailureActionSheet(
|
|
from: viewController,
|
|
supportTag: "CorruptRegistrationState",
|
|
title: OWSLocalizedString(
|
|
"APP_LAUNCH_FAILURE_CORRUPT_REGISTRATION_TITLE",
|
|
comment: "Title for an error indicating that the app couldn't launch because some unexpected error happened with the user's registration status."
|
|
),
|
|
message: OWSLocalizedString(
|
|
"APP_LAUNCH_FAILURE_CORRUPT_REGISTRATION_MESSAGE",
|
|
comment: "Message for an error indicating that the app couldn't launch because some unexpected error happened with the user's registration status."
|
|
),
|
|
actions: [.submitDebugLogsAndCrash]
|
|
)
|
|
case nil:
|
|
let backgroundTask = OWSBackgroundTask(label: #function)
|
|
Task { @MainActor in
|
|
defer { backgroundTask.end() }
|
|
if !hasInProgressRegistration {
|
|
await LaunchJobs.run(databaseStorage: SSKEnvironment.shared.databaseStorageRef)
|
|
}
|
|
DispatchQueue.main.async {
|
|
self.setAppIsReady(
|
|
launchInterface: launchInterface,
|
|
launchContext: launchContext
|
|
)
|
|
DeviceSleepManager.shared.removeBlock(blockObject: sleepBlockObject)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func setAppIsReady(
|
|
launchInterface: LaunchInterface,
|
|
launchContext: LaunchContext
|
|
) {
|
|
owsPrecondition(!appReadiness.isAppReady)
|
|
owsPrecondition(!CurrentAppContext().isRunningTests)
|
|
|
|
let appContext = launchContext.appContext
|
|
|
|
SignalApp.shared.performInitialSetup(appReadiness: appReadiness)
|
|
|
|
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
|
|
// This runs every 24 hours or so.
|
|
let messageSendLog = SSKEnvironment.shared.messageSendLogRef
|
|
messageSendLog.cleanUpAndScheduleNextOccurrence(on: DependenciesBridge.shared.schedulers)
|
|
}
|
|
|
|
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
|
|
OWSOrphanDataCleaner.auditOnLaunchIfNecessary()
|
|
}
|
|
|
|
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
|
|
Task.detached(priority: .low) {
|
|
await FullTextSearchOptimizer(
|
|
appContext: appContext,
|
|
db: DependenciesBridge.shared.db
|
|
).run()
|
|
}
|
|
}
|
|
|
|
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
|
|
Task.detached(priority: .low) {
|
|
await AuthorMergeHelperBuilder(
|
|
appContext: appContext,
|
|
authorMergeHelper: DependenciesBridge.shared.authorMergeHelper,
|
|
db: DependenciesBridge.shared.db,
|
|
modelReadCaches: AuthorMergeHelperBuilder.Wrappers.ModelReadCaches(SSKEnvironment.shared.modelReadCachesRef),
|
|
recipientDatabaseTable: DependenciesBridge.shared.recipientDatabaseTable
|
|
).buildTableIfNeeded()
|
|
}
|
|
}
|
|
|
|
// Disable phone number sharing when rolling out PNP.
|
|
//
|
|
// TODO: Remove this once all builds are PNP-enabled.
|
|
//
|
|
// Once all builds are PNP enabled, we can remove this explicit migration
|
|
// and simply treat the default as "nobody". The migration exists to ensure
|
|
// old linked devices respect the setting before they upgrade.
|
|
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
|
|
let db = DependenciesBridge.shared.db
|
|
guard db.read(block: SSKEnvironment.shared.udManagerRef.phoneNumberSharingMode(tx:)) == nil else {
|
|
return
|
|
}
|
|
db.write { tx in
|
|
guard SSKEnvironment.shared.udManagerRef.phoneNumberSharingMode(tx: tx) == nil else {
|
|
return
|
|
}
|
|
SSKEnvironment.shared.udManagerRef.setPhoneNumberSharingMode(
|
|
.nobody,
|
|
updateStorageServiceAndProfile: true,
|
|
tx: SDSDB.shimOnlyBridge(tx)
|
|
)
|
|
}
|
|
}
|
|
|
|
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
|
|
StaleProfileFetcher(
|
|
db: DependenciesBridge.shared.db,
|
|
profileFetcher: SSKEnvironment.shared.profileFetcherRef,
|
|
tsAccountManager: DependenciesBridge.shared.tsAccountManager
|
|
).scheduleProfileFetches()
|
|
}
|
|
|
|
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
|
|
Task.detached(priority: .low) {
|
|
YDBStorage.deleteYDBStorage()
|
|
SSKPreferences.clearLegacyDatabaseFlags(from: appContext.appUserDefaults())
|
|
try? launchContext.keychainStorage.removeValue(service: "TSKeyChainService", key: "TSDatabasePass")
|
|
try? launchContext.keychainStorage.removeValue(service: "TSKeyChainService", key: "OWSDatabaseCipherKeySpec")
|
|
}
|
|
}
|
|
|
|
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
|
|
Task {
|
|
try? await RemoteMegaphoneFetcher(
|
|
databaseStorage: SSKEnvironment.shared.databaseStorageRef,
|
|
signalService: SSKEnvironment.shared.signalServiceRef
|
|
).syncRemoteMegaphonesIfNecessary()
|
|
}
|
|
}
|
|
|
|
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
|
|
DependenciesBridge.shared.orphanedAttachmentCleaner.beginObserving()
|
|
}
|
|
|
|
appReadiness.runNowOrWhenMainAppDidBecomeReadyAsync {
|
|
AttachmentDownloadRetryRunner.shared.beginObserving()
|
|
}
|
|
|
|
appReadiness.runNowOrWhenMainAppDidBecomeReadyAsync {
|
|
let fetchJobRunner = CallLinkFetchJobRunner(
|
|
callLinkStore: DependenciesBridge.shared.callLinkStore,
|
|
callLinkStateUpdater: AppEnvironment.shared.callService.callLinkStateUpdater,
|
|
db: DependenciesBridge.shared.db
|
|
)
|
|
fetchJobRunner.observeDatabase(DependenciesBridge.shared.databaseChangeObserver)
|
|
fetchJobRunner.setMightHavePendingFetchAndFetch()
|
|
AppEnvironment.shared.ownedObjects.append(fetchJobRunner)
|
|
}
|
|
|
|
// Note that this does much more than set a flag; it will also run all deferred blocks.
|
|
appReadiness.setAppIsReadyUIStillPending()
|
|
|
|
appContext.appUserDefaults().removeObject(forKey: Constants.appLaunchesAttemptedKey)
|
|
|
|
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
|
let recipientDatabaseTable = DependenciesBridge.shared.recipientDatabaseTable
|
|
let tsRegistrationState: TSRegistrationState = SSKEnvironment.shared.databaseStorageRef.read { tx in
|
|
let registrationState = tsAccountManager.registrationState(tx: tx.asV2Read)
|
|
if registrationState.isRegistered, let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx.asV2Read) {
|
|
let deviceId = tsAccountManager.storedDeviceId(tx: tx.asV2Read)
|
|
let localRecipient = recipientDatabaseTable.fetchRecipient(serviceId: localIdentifiers.aci, transaction: tx.asV2Read)
|
|
let deviceCount = localRecipient?.deviceIds.count ?? 0
|
|
let linkedDeviceMessage = deviceCount > 1 ? "\(deviceCount) devices including the primary" : "no linked devices"
|
|
Logger.info("localAci: \(localIdentifiers.aci), deviceId: \(deviceId) (\(linkedDeviceMessage))")
|
|
}
|
|
return registrationState
|
|
}
|
|
|
|
if tsRegistrationState.isRegistered {
|
|
// This should happen at any launch, background or foreground.
|
|
SyncPushTokensJob.run()
|
|
}
|
|
|
|
if tsRegistrationState.isRegistered {
|
|
APNSRotationStore.rotateIfNeededOnAppLaunchAndReadiness(performRotation: {
|
|
SyncPushTokensJob.run(mode: .rotateIfEligible)
|
|
}).map {
|
|
// If the method returns a closure, run it after message processing.
|
|
_ = SSKEnvironment.shared.messageProcessorRef.waitForFetchingAndProcessing().done($0)
|
|
}
|
|
}
|
|
|
|
if tsRegistrationState.isRegistered {
|
|
Task {
|
|
do {
|
|
_ = try await SSKEnvironment.shared.profileManagerRef.fetchLocalUsersProfile(
|
|
authedAccount: .implicit()
|
|
).awaitable()
|
|
try await SSKEnvironment.shared.profileManagerRef.downloadAndDecryptLocalUserAvatarIfNeeded(
|
|
authedAccount: .implicit()
|
|
)
|
|
} catch {
|
|
Logger.warn("Couldn't fetch local user profile or avatar: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
DebugLogger.shared.postLaunchLogCleanup(appContext: appContext)
|
|
AppVersionImpl.shared.mainAppLaunchDidComplete()
|
|
|
|
scheduleBgAppRefresh()
|
|
Self.updateApplicationShortcutItems(isRegistered: tsRegistrationState.isRegistered)
|
|
|
|
let notificationCenter = NotificationCenter.default
|
|
notificationCenter.addObserver(
|
|
self,
|
|
selector: #selector(registrationStateDidChange),
|
|
name: .registrationStateDidChange,
|
|
object: nil
|
|
)
|
|
|
|
checkDatabaseIntegrityIfNecessary(isRegistered: tsRegistrationState.isRegistered)
|
|
|
|
SignalApp.shared.showLaunchInterface(
|
|
launchInterface,
|
|
appReadiness: appReadiness,
|
|
launchStartedAt: launchContext.launchStartedAt
|
|
)
|
|
}
|
|
|
|
private func scheduleBgAppRefresh() {
|
|
MessageFetchBGRefreshTask.getShared(appReadiness: appReadiness)?.scheduleTask()
|
|
}
|
|
|
|
/// The user must unlock the device once after reboot before the database encryption key can be accessed.
|
|
private func notifyThatPhoneMustBeUnlocked() -> Never {
|
|
Logger.warn("Exiting because we are in the background and the database password is not accessible.")
|
|
|
|
let notificationContent = UNMutableNotificationContent()
|
|
notificationContent.body = String(
|
|
format: OWSLocalizedString(
|
|
"NOTIFICATION_BODY_PHONE_LOCKED_FORMAT",
|
|
comment: "Lock screen notification text presented after user powers on their device without unlocking. Embeds {{device model}} (either 'iPad' or 'iPhone')"
|
|
),
|
|
UIDevice.current.localizedModel
|
|
)
|
|
|
|
let notificationRequest = UNNotificationRequest(
|
|
identifier: UUID().uuidString,
|
|
content: notificationContent,
|
|
trigger: nil
|
|
)
|
|
|
|
let application: UIApplication = .shared
|
|
let userNotificationCenter: UNUserNotificationCenter = .current()
|
|
|
|
NotificationPresenterImpl.clearAllNotificationsExceptNewLinkedDevices()
|
|
application.applicationIconBadgeNumber = 0
|
|
|
|
userNotificationCenter.add(notificationRequest)
|
|
application.applicationIconBadgeNumber = 1
|
|
|
|
// Wait a few seconds for XPC calls to finish and for rate limiting purposes.
|
|
Thread.sleep(forTimeInterval: 3)
|
|
Logger.flush()
|
|
exit(0)
|
|
}
|
|
|
|
// MARK: - Registration
|
|
|
|
private func buildLaunchInterface(regLoader: RegistrationCoordinatorLoader) -> LaunchInterface {
|
|
let (
|
|
tsRegistrationState,
|
|
lastMode
|
|
) = SSKEnvironment.shared.databaseStorageRef.read { tx in
|
|
return (
|
|
DependenciesBridge.shared.tsAccountManager.registrationState(tx: tx.asV2Read),
|
|
regLoader.restoreLastMode(transaction: tx.asV2Read)
|
|
)
|
|
}
|
|
|
|
if let lastMode {
|
|
Logger.info("Found ongoing registration; continuing")
|
|
return .registration(regLoader, lastMode)
|
|
}
|
|
switch tsRegistrationState {
|
|
case .registered, .provisioned:
|
|
// We're already registered.
|
|
return .chatList
|
|
|
|
case .reregistering(let reregNumber, let reregAci):
|
|
if let reregE164 = E164(reregNumber), let reregAci {
|
|
Logger.info("Found legacy re-registration; continuing in new registration")
|
|
// A user who started re-registration before the new
|
|
// registration flow shipped; kick them to new re-reg.
|
|
return .registration(regLoader, .reRegistering(.init(e164: reregE164, aci: reregAci)))
|
|
} else {
|
|
// If we're missing the e164 or aci, drop into normal reg.
|
|
Logger.info("Found legacy initial registration; continuing in new registration")
|
|
return .registration(regLoader, .registering)
|
|
}
|
|
|
|
case .relinking:
|
|
return .secondaryProvisioning
|
|
|
|
case .deregistered:
|
|
// If we are deregistered, go to the chat list in the deregistered state.
|
|
// The user can kick of re-registration from there, which will set the
|
|
// 'lastMode' var and short circuit before we get here next time around.
|
|
return .chatList
|
|
|
|
case .delinked:
|
|
// If we are delinked, go to the chat list in the delinked state.
|
|
// The user can kick of re-linking from there.
|
|
return .chatList
|
|
|
|
case
|
|
.transferringIncoming,
|
|
.transferringPrimaryOutgoing,
|
|
.transferringLinkedOutgoing,
|
|
.transferred:
|
|
fallthrough
|
|
|
|
case .unregistered:
|
|
if UIDevice.current.isIPad {
|
|
return .secondaryProvisioning
|
|
} else {
|
|
return .registration(regLoader, .registering)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Launch Failures
|
|
|
|
private var didAppLaunchFail: Bool = false {
|
|
didSet {
|
|
if !didAppLaunchFail {
|
|
self.shouldKillAppWhenBackgrounded = false
|
|
}
|
|
}
|
|
}
|
|
|
|
private var shouldKillAppWhenBackgrounded: Bool = false
|
|
|
|
private func checkIfAllowedToLaunch(
|
|
mainAppContext: MainAppContext,
|
|
appVersion: AppVersion,
|
|
incrementalTSAttachmentMigrationStore: IncrementalTSAttachmentMigrationStore,
|
|
didDeviceTransferRestoreSucceed: Bool
|
|
) -> LaunchPreflightError? {
|
|
guard checkSomeDiskSpaceAvailable() else {
|
|
return .lowStorageSpaceAvailable
|
|
}
|
|
|
|
guard didDeviceTransferRestoreSucceed else {
|
|
return .couldNotRestoreTransferredData
|
|
}
|
|
|
|
// Prevent:
|
|
// * Users with an unknown GRDB schema revert to using an earlier GRDB schema.
|
|
if SSKPreferences.hasUnknownGRDBSchema() {
|
|
return .unknownDatabaseVersion
|
|
}
|
|
|
|
let userDefaults = mainAppContext.appUserDefaults()
|
|
|
|
let databaseCorruptionState = DatabaseCorruptionState(userDefaults: userDefaults)
|
|
switch databaseCorruptionState.status {
|
|
case .notCorrupted, .readCorrupted:
|
|
break
|
|
case .corrupted, .corruptedButAlreadyDumpedAndRestored:
|
|
guard !UIDevice.current.isIPad else {
|
|
// Database recovery theoretically works on iPad,
|
|
// but we haven't built the UI for it.
|
|
return .databaseUnrecoverablyCorrupted
|
|
}
|
|
guard databaseCorruptionState.count <= 3 else {
|
|
return .databaseUnrecoverablyCorrupted
|
|
}
|
|
return .databaseCorruptedAndMightBeRecoverable
|
|
}
|
|
|
|
let launchAttemptFailureThreshold = DebugFlags.betaLogging ? 2 : 3
|
|
if
|
|
appVersion.lastAppVersion == appVersion.currentAppVersion,
|
|
userDefaults.integer(forKey: Constants.appLaunchesAttemptedKey) >= launchAttemptFailureThreshold
|
|
{
|
|
if case .readCorrupted = databaseCorruptionState.status {
|
|
return .possibleReadCorruptionCrashed
|
|
}
|
|
return .lastAppLaunchCrashed
|
|
}
|
|
|
|
if incrementalTSAttachmentMigrationStore.shouldReportFailureInUI() {
|
|
if let logString = incrementalTSAttachmentMigrationStore.consumeLastBGProcessingTaskError() {
|
|
Logger.error("Failed TSAttachment migration in BGProcessingTask: \(logString)")
|
|
}
|
|
return .incrementalTSAttachmentMigrationFailed
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private func showPreflightErrorUI(
|
|
_ preflightError: LaunchPreflightError,
|
|
launchContext: LaunchContext,
|
|
window: UIWindow,
|
|
viewController: UIViewController
|
|
) {
|
|
Logger.warn("preflightError: \(preflightError)")
|
|
|
|
let title: String
|
|
let message: String
|
|
let actions: [LaunchFailureActionSheetAction]
|
|
|
|
switch preflightError {
|
|
case .databaseCorruptedAndMightBeRecoverable, .possibleReadCorruptionCrashed:
|
|
presentDatabaseRecovery(
|
|
from: viewController,
|
|
launchContext: launchContext,
|
|
window: window
|
|
)
|
|
return
|
|
|
|
case .databaseUnrecoverablyCorrupted:
|
|
presentDatabaseUnrecoverablyCorruptedError(
|
|
from: viewController,
|
|
action: .submitDebugLogsWithDatabaseIntegrityCheckAndCrash(databaseStorage: launchContext.databaseStorage)
|
|
)
|
|
return
|
|
|
|
case .unknownDatabaseVersion:
|
|
title = OWSLocalizedString(
|
|
"APP_LAUNCH_FAILURE_INVALID_DATABASE_VERSION_TITLE",
|
|
comment: "Error indicating that the app could not launch without reverting unknown database migrations."
|
|
)
|
|
message = OWSLocalizedString(
|
|
"APP_LAUNCH_FAILURE_INVALID_DATABASE_VERSION_MESSAGE",
|
|
comment: "Error indicating that the app could not launch without reverting unknown database migrations."
|
|
)
|
|
actions = [.submitDebugLogsAndCrash]
|
|
|
|
case .couldNotRestoreTransferredData:
|
|
title = OWSLocalizedString(
|
|
"APP_LAUNCH_FAILURE_RESTORE_FAILED_TITLE",
|
|
comment: "Error indicating that the app could not restore transferred data."
|
|
)
|
|
message = OWSLocalizedString(
|
|
"APP_LAUNCH_FAILURE_RESTORE_FAILED_MESSAGE",
|
|
comment: "Error indicating that the app could not restore transferred data."
|
|
)
|
|
actions = [.submitDebugLogsAndCrash]
|
|
|
|
case .lastAppLaunchCrashed, .incrementalTSAttachmentMigrationFailed:
|
|
title = OWSLocalizedString(
|
|
"APP_LAUNCH_FAILURE_LAST_LAUNCH_CRASHED_TITLE",
|
|
comment: "Error indicating that the app crashed during the previous launch."
|
|
)
|
|
message = OWSLocalizedString(
|
|
"APP_LAUNCH_FAILURE_LAST_LAUNCH_CRASHED_MESSAGE",
|
|
comment: "Error indicating that the app crashed during the previous launch."
|
|
)
|
|
actions = [
|
|
.submitDebugLogsAndLaunchApp(window: window, launchContext: launchContext),
|
|
.launchApp(window: window, launchContext: launchContext)
|
|
]
|
|
|
|
case .lowStorageSpaceAvailable:
|
|
shouldKillAppWhenBackgrounded = true
|
|
title = OWSLocalizedString(
|
|
"APP_LAUNCH_FAILURE_LOW_STORAGE_SPACE_AVAILABLE_TITLE",
|
|
comment: "Error title indicating that the app crashed because there was low storage space available on the device."
|
|
)
|
|
message = OWSLocalizedString(
|
|
"APP_LAUNCH_FAILURE_LOW_STORAGE_SPACE_AVAILABLE_MESSAGE",
|
|
comment: "Error description indicating that the app crashed because there was low storage space available on the device."
|
|
)
|
|
actions = []
|
|
}
|
|
|
|
presentLaunchFailureActionSheet(
|
|
from: viewController,
|
|
supportTag: preflightError.supportTag,
|
|
title: title,
|
|
message: message,
|
|
actions: actions
|
|
)
|
|
}
|
|
|
|
private func presentDatabaseRecovery(
|
|
from viewController: UIViewController,
|
|
launchContext: LaunchContext,
|
|
window: UIWindow
|
|
) {
|
|
var launchContext = launchContext
|
|
let recoveryViewController = DatabaseRecoveryViewController<(AppSetup.FinalContinuation, DeviceSleepManager.BlockObject)>(
|
|
appReadiness: appReadiness,
|
|
corruptDatabaseStorage: launchContext.databaseStorage,
|
|
keychainStorage: launchContext.keychainStorage,
|
|
setupSskEnvironment: { databaseStorage in
|
|
firstly(on: DispatchQueue.main) {
|
|
launchContext.databaseStorage = databaseStorage
|
|
return self.setUpMainAppEnvironment(
|
|
launchContext: launchContext
|
|
)
|
|
}
|
|
},
|
|
launchApp: { (finalContinuation, sleepBlockObject) in
|
|
// Pretend we didn't fail!
|
|
self.didAppLaunchFail = false
|
|
self.configureGlobalUI(in: window)
|
|
self.didLoadDatabase(
|
|
finalContinuation: finalContinuation,
|
|
launchContext: launchContext,
|
|
sleepBlockObject: sleepBlockObject,
|
|
window: window
|
|
)
|
|
}
|
|
)
|
|
|
|
// Prevent dismissal.
|
|
recoveryViewController.isModalInPresentation = true
|
|
|
|
if let presentationController = recoveryViewController.sheetPresentationController {
|
|
presentationController.detents = [.medium()]
|
|
presentationController.prefersEdgeAttachedInCompactHeight = true
|
|
presentationController.widthFollowsPreferredContentSizeWhenEdgeAttached = true
|
|
}
|
|
|
|
viewController.present(recoveryViewController, animated: true)
|
|
}
|
|
|
|
private func presentDatabaseUnrecoverablyCorruptedError(
|
|
from viewController: UIViewController,
|
|
action: LaunchFailureActionSheetAction
|
|
) {
|
|
presentLaunchFailureActionSheet(
|
|
from: viewController,
|
|
supportTag: LaunchPreflightError.databaseUnrecoverablyCorrupted.supportTag,
|
|
title: OWSLocalizedString(
|
|
"APP_LAUNCH_FAILURE_COULD_NOT_LOAD_DATABASE",
|
|
comment: "Error indicating that the app could not launch because the database could not be loaded."
|
|
),
|
|
message: OWSLocalizedString(
|
|
"APP_LAUNCH_FAILURE_ALERT_MESSAGE",
|
|
comment: "Default message for the 'app launch failed' alert."
|
|
),
|
|
actions: [action]
|
|
)
|
|
}
|
|
|
|
private enum LaunchFailureActionSheetAction {
|
|
case submitDebugLogsAndCrash
|
|
case submitDebugLogsAndLaunchApp(window: UIWindow, launchContext: LaunchContext)
|
|
case submitDebugLogsWithDatabaseIntegrityCheckAndCrash(databaseStorage: SDSDatabaseStorage)
|
|
case launchApp(window: UIWindow, launchContext: LaunchContext)
|
|
}
|
|
|
|
private func presentLaunchFailureActionSheet(
|
|
from viewController: UIViewController,
|
|
supportTag: String,
|
|
title: String,
|
|
message: String,
|
|
actions: [LaunchFailureActionSheetAction]
|
|
) {
|
|
let actionSheet = ActionSheetController(title: title, message: message)
|
|
|
|
if DebugFlags.internalSettings {
|
|
actionSheet.addAction(.init(title: "Export Database (internal)") { [unowned viewController] _ in
|
|
SignalApp.showExportDatabaseUI(from: viewController) {
|
|
self.presentLaunchFailureActionSheet(
|
|
from: viewController,
|
|
supportTag: supportTag,
|
|
title: title,
|
|
message: message,
|
|
actions: actions
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
func addSubmitDebugLogsAction(handler: @escaping () -> Void) {
|
|
let actionTitle = OWSLocalizedString("SETTINGS_ADVANCED_SUBMIT_DEBUGLOG", comment: "")
|
|
actionSheet.addAction(.init(title: actionTitle) { _ in
|
|
handler()
|
|
})
|
|
}
|
|
|
|
func ignoreErrorAndLaunchApp(in window: UIWindow, launchContext: LaunchContext) {
|
|
// Pretend we didn't fail!
|
|
self.didAppLaunchFail = false
|
|
launchContext.incrementalMessageTSAttachmentMigrationStore.didReportFailureInUI()
|
|
let loadingViewController = LoadingViewController()
|
|
window.rootViewController = loadingViewController
|
|
self.launchApp(in: window, launchContext: launchContext)
|
|
}
|
|
|
|
for action in actions {
|
|
switch action {
|
|
case .submitDebugLogsAndCrash:
|
|
addSubmitDebugLogsAction {
|
|
DebugLogs.submitLogsWithSupportTag(supportTag) {
|
|
owsFail("Exiting after submitting debug logs")
|
|
}
|
|
}
|
|
case .submitDebugLogsAndLaunchApp(let window, let launchContext):
|
|
addSubmitDebugLogsAction { [unowned window] in
|
|
DebugLogs.submitLogsWithSupportTag(supportTag) {
|
|
ignoreErrorAndLaunchApp(in: window, launchContext: launchContext)
|
|
}
|
|
}
|
|
case .submitDebugLogsWithDatabaseIntegrityCheckAndCrash(let databaseStorage):
|
|
addSubmitDebugLogsAction { [unowned viewController] in
|
|
SignalApp.showDatabaseIntegrityCheckUI(from: viewController, databaseStorage: databaseStorage) {
|
|
DebugLogs.submitLogsWithSupportTag(supportTag) {
|
|
owsFail("Exiting after submitting debug logs")
|
|
}
|
|
}
|
|
}
|
|
case .launchApp(let window, let launchContext):
|
|
actionSheet.addAction(.init(
|
|
title: OWSLocalizedString(
|
|
"APP_LAUNCH_FAILURE_CONTINUE",
|
|
comment: "Button to try launching the app even though the last launch failed"
|
|
),
|
|
style: .cancel, // Use a cancel-style button to draw attention.
|
|
handler: { [unowned window] _ in
|
|
ignoreErrorAndLaunchApp(in: window, launchContext: launchContext)
|
|
}
|
|
))
|
|
}
|
|
}
|
|
|
|
viewController.presentActionSheet(actionSheet)
|
|
}
|
|
|
|
public func userNotificationCenter(
|
|
_ center: UNUserNotificationCenter,
|
|
willPresent notification: UNNotification,
|
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
|
) {
|
|
appReadiness.runNowOrWhenAppDidBecomeReadySync {
|
|
// We need to respect the in-app notification sound preference. This method, which is called
|
|
// for modern UNUserNotification users, could be a place to do that, but since we'd still
|
|
// need to handle this behavior for legacy UINotification users anyway, we "allow" all
|
|
// notification options here, and rely on the shared logic in NotificationPresenterImpl to
|
|
// honor notification sound preferences for both modern and legacy users.
|
|
let options: UNNotificationPresentationOptions = [.badge, .banner, .list, .sound]
|
|
completionHandler(options)
|
|
}
|
|
}
|
|
|
|
private func terminalErrorViewController() -> UIViewController {
|
|
let storyboard = UIStoryboard(name: "Launch Screen", bundle: nil)
|
|
guard let viewController = storyboard.instantiateInitialViewController() else {
|
|
owsFail("No initial view controller")
|
|
}
|
|
return viewController
|
|
}
|
|
|
|
// MARK: - Activation
|
|
|
|
private var hasActivated = false
|
|
|
|
private func handleActivation() {
|
|
AssertIsOnMainThread()
|
|
|
|
defer {
|
|
Logger.info("Activated.")
|
|
}
|
|
|
|
let tsRegistrationState: TSRegistrationState = DependenciesBridge.shared.db.read { tx in
|
|
// Always check prekeys after app launches, and sometimes check on app activation.
|
|
let registrationState = DependenciesBridge.shared.tsAccountManager.registrationState(tx: tx)
|
|
if registrationState.isRegistered {
|
|
DependenciesBridge.shared.preKeyManager.checkPreKeysIfNecessary(tx: tx)
|
|
}
|
|
return registrationState
|
|
}
|
|
|
|
if !hasActivated {
|
|
hasActivated = true
|
|
|
|
RTCInitializeSSL()
|
|
|
|
// Clean up any messages that expired since last launch and continue
|
|
// cleaning in the background.
|
|
SSKEnvironment.shared.disappearingMessagesJobRef.startIfNecessary()
|
|
|
|
if !tsRegistrationState.isRegistered {
|
|
// Unregistered user should have no unread messages. e.g. if you delete your account.
|
|
SSKEnvironment.shared.notificationPresenterRef.clearAllNotifications()
|
|
}
|
|
}
|
|
|
|
// Every time we become active...
|
|
if tsRegistrationState.isRegistered {
|
|
// At this point, potentially lengthy DB locking migrations could be running.
|
|
// Avoid blocking app launch by putting all further possible DB access in async block
|
|
DispatchQueue.main.async {
|
|
SSKEnvironment.shared.contactManagerImplRef.fetchSystemContactsOnceIfAlreadyAuthorized()
|
|
|
|
// TODO: Should we run this immediately even if we would like to process
|
|
// already decrypted envelopes handed to us by the NSE?
|
|
_ = SSKEnvironment.shared.messageFetcherJobRef.run()
|
|
|
|
if !UIApplication.shared.isRegisteredForRemoteNotifications {
|
|
Logger.info("Retrying to register for remote notifications since user hasn't registered yet.")
|
|
// Push tokens don't normally change while the app is launched, so checking once during launch is
|
|
// usually sufficient, but e.g. on iOS11, users who have disabled "Allow Notifications" and disabled
|
|
// "Background App Refresh" will not be able to obtain an APN token. Enabling those settings does not
|
|
// restart the app, so we check every activation for users who haven't yet registered.
|
|
SyncPushTokensJob.run()
|
|
}
|
|
}
|
|
}
|
|
|
|
// We want to defer this so that we never call this method until
|
|
// [UIApplicationDelegate applicationDidBecomeActive:] is complete.
|
|
let identityManager = DependenciesBridge.shared.identityManager
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { identityManager.tryToSyncQueuedVerificationStates() }
|
|
}
|
|
|
|
// MARK: - Orientation
|
|
|
|
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
|
|
if CurrentAppContext().isRunningTests || didAppLaunchFail {
|
|
return .portrait
|
|
}
|
|
|
|
// The call-banner window is only suitable for portrait display on iPhone
|
|
if appReadiness.isAppReady, AppEnvironment.shared.callService.callServiceState.currentCall != nil, !UIDevice.current.isIPad {
|
|
return .portrait
|
|
}
|
|
|
|
guard let rootViewController = self.window?.rootViewController else {
|
|
return UIDevice.current.defaultSupportedOrientations
|
|
}
|
|
|
|
return rootViewController.supportedInterfaceOrientations
|
|
}
|
|
|
|
// MARK: - Notifications
|
|
|
|
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
AssertIsOnMainThread()
|
|
|
|
if didAppLaunchFail {
|
|
return
|
|
}
|
|
|
|
Logger.info("")
|
|
AppEnvironment.shared.pushRegistrationManagerRef.didReceiveVanillaPushToken(deviceToken)
|
|
}
|
|
|
|
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
|
AssertIsOnMainThread()
|
|
|
|
if didAppLaunchFail {
|
|
return
|
|
}
|
|
|
|
Logger.warn("")
|
|
#if DEBUG
|
|
AppEnvironment.shared.pushRegistrationManagerRef.didReceiveVanillaPushToken(Data(count: 32))
|
|
#else
|
|
AppEnvironment.shared.pushRegistrationManagerRef.didFailToReceiveVanillaPushToken(error: error)
|
|
#endif
|
|
}
|
|
|
|
func application(
|
|
_ application: UIApplication,
|
|
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
|
|
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
|
|
) {
|
|
AssertIsOnMainThread()
|
|
|
|
if DebugFlags.verboseNotificationLogging {
|
|
Logger.info("")
|
|
}
|
|
|
|
// Mark down that the APNS token is working because we got a push.
|
|
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
|
|
SSKEnvironment.shared.databaseStorageRef.asyncWrite { tx in
|
|
APNSRotationStore.didReceiveAPNSPush(transaction: tx)
|
|
}
|
|
}
|
|
|
|
processRemoteNotification(userInfo) {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 20) {
|
|
completionHandler(.newData)
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum HandleSilentPushContentResult {
|
|
case handled
|
|
case notHandled
|
|
}
|
|
|
|
// TODO: NSE Lifecycle, is this invoked when the NSE wakes the main app?
|
|
private func processRemoteNotification(_ remoteNotification: [AnyHashable: Any], completion: @escaping () -> Void) {
|
|
AssertIsOnMainThread()
|
|
|
|
appReadiness.runNowOrWhenAppDidBecomeReadySync {
|
|
// TODO: Wait to invoke this until we've finished fetching messages.
|
|
defer { completion() }
|
|
|
|
switch self.handleSilentPushContent(remoteNotification) {
|
|
case .handled:
|
|
break
|
|
case .notHandled:
|
|
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
|
guard tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
|
|
Logger.info("Ignoring remote notification; user is not registered.")
|
|
return
|
|
}
|
|
_ = SSKEnvironment.shared.messageFetcherJobRef.run()
|
|
// If the main app gets woken to process messages in the background, check
|
|
// for any pending NSE requests to fulfill.
|
|
_ = SSKEnvironment.shared.syncManagerRef.syncAllContactsIfFullSyncRequested()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handleSilentPushContent(_ remoteNotification: [AnyHashable: Any]) -> HandleSilentPushContentResult {
|
|
if let spamChallengeToken = remoteNotification["rateLimitChallenge"] as? String {
|
|
SSKEnvironment.shared.spamChallengeResolverRef.handleIncomingPushChallengeToken(spamChallengeToken)
|
|
return .handled
|
|
}
|
|
|
|
if let preAuthChallengeToken = remoteNotification["challenge"] as? String {
|
|
AppEnvironment.shared.pushRegistrationManagerRef.didReceiveVanillaPreAuthChallengeToken(preAuthChallengeToken)
|
|
return .handled
|
|
}
|
|
|
|
return .notHandled
|
|
}
|
|
|
|
private func clearAppropriateNotificationsAndRestoreBadgeCount() {
|
|
AssertIsOnMainThread()
|
|
|
|
appReadiness.runNowOrWhenAppDidBecomeReadySync {
|
|
let oldBadgeValue = UIApplication.shared.applicationIconBadgeNumber
|
|
SSKEnvironment.shared.notificationPresenterRef.clearAllNotificationsExceptNewLinkedDevices()
|
|
UIApplication.shared.applicationIconBadgeNumber = oldBadgeValue
|
|
}
|
|
}
|
|
|
|
// MARK: - Handoff
|
|
|
|
/// Among other things, this is used by "call back" CallKit dialog and calling from the Contacts app.
|
|
///
|
|
/// We always return true if we are going to try to handle the user activity
|
|
/// since we never want iOS to contact us again using a URL.
|
|
///
|
|
/// From https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623072-application?language=objc:
|
|
///
|
|
/// If you do not implement this method or if your implementation returns
|
|
/// false, iOS tries to create a document for your app to open using a URL.
|
|
@available(iOS, deprecated: 13.0) // hack to mute deprecation warnings; this is not deprecated
|
|
func application(
|
|
_ application: UIApplication,
|
|
continue userActivity: NSUserActivity,
|
|
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
|
|
) -> Bool {
|
|
AssertIsOnMainThread()
|
|
|
|
if didAppLaunchFail {
|
|
return false
|
|
}
|
|
|
|
Logger.info("\(userActivity.activityType)")
|
|
|
|
switch userActivity.activityType {
|
|
case "INSendMessageIntent":
|
|
let intent = userActivity.interaction?.intent
|
|
guard let intent = intent as? INSendMessageIntent else {
|
|
owsFailDebug("Wrong type for intent: \(type(of: intent))")
|
|
return false
|
|
}
|
|
guard let threadUniqueId = intent.conversationIdentifier else {
|
|
owsFailDebug("Missing threadUniqueId for intent")
|
|
return false
|
|
}
|
|
appReadiness.runNowOrWhenAppDidBecomeReadySync {
|
|
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
|
guard tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
|
|
Logger.warn("Ignoring user activity; not registered.")
|
|
return
|
|
}
|
|
SignalApp.shared.presentConversationAndScrollToFirstUnreadMessage(forThreadId: threadUniqueId, animated: false)
|
|
}
|
|
return true
|
|
case "INStartVideoCallIntent":
|
|
return handleStartCallIntent(
|
|
INStartVideoCallIntent.self,
|
|
userActivity: userActivity,
|
|
contacts: \.contacts,
|
|
isVideoCall: { _ in true }
|
|
)
|
|
case "INStartAudioCallIntent":
|
|
return handleStartCallIntent(
|
|
INStartAudioCallIntent.self,
|
|
userActivity: userActivity,
|
|
contacts: \.contacts,
|
|
isVideoCall: { _ in false }
|
|
)
|
|
case "INStartCallIntent":
|
|
return handleStartCallIntent(
|
|
INStartCallIntent.self,
|
|
userActivity: userActivity,
|
|
contacts: \.contacts,
|
|
isVideoCall: { $0.callCapability == .videoCall }
|
|
)
|
|
case NSUserActivityTypeBrowsingWeb:
|
|
guard let webpageUrl = userActivity.webpageURL else {
|
|
owsFailDebug("Missing webpageUrl.")
|
|
return false
|
|
}
|
|
return handleOpenUrl(webpageUrl)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
private func handleStartCallIntent<T: INIntent>(
|
|
_ intentType: T.Type,
|
|
userActivity: NSUserActivity,
|
|
contacts: KeyPath<T, [INPerson]?>,
|
|
isVideoCall: (T) -> Bool
|
|
) -> Bool {
|
|
let intent = userActivity.interaction?.intent
|
|
guard let intent = intent as? T else {
|
|
owsFailDebug("Wrong type for intent: \(type(of: intent))")
|
|
return false
|
|
}
|
|
guard let handle = intent[keyPath: contacts]?.first?.personHandle?.value else {
|
|
owsFailDebug("Missing handle for intent")
|
|
return false
|
|
}
|
|
let isVideo = isVideoCall(intent)
|
|
appReadiness.runNowOrWhenAppDidBecomeReadySync {
|
|
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
|
guard tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
|
|
Logger.warn("Ignoring user activity; not registered.")
|
|
return
|
|
}
|
|
guard let callTarget = CallKitCallManager.callTargetForHandleWithSneakyTransaction(handle) else {
|
|
Logger.warn("Ignoring user activity; unknown user.")
|
|
return
|
|
}
|
|
// This intent can be received from more than one user interaction.
|
|
//
|
|
// * It can be received if the user taps the "video" button in the CallKit
|
|
// UI for an an ongoing call. If so, the correct response is to try to
|
|
// activate the local video for that call.
|
|
//
|
|
// * It can be received if the user taps the "video" button for a contact
|
|
// in the contacts app. If so, the correct response is to try to initiate a
|
|
// new call to that user - unless there is another call in progress.
|
|
let callService = AppEnvironment.shared.callService!
|
|
if let currentCall = callService.callServiceState.currentCall {
|
|
if isVideo, case .individual = currentCall.mode, currentCall.mode.matches(callTarget) {
|
|
Logger.info("Upgrading existing call to video")
|
|
callService.updateIsLocalVideoMuted(isLocalVideoMuted: false)
|
|
} else {
|
|
Logger.warn("Ignoring user activity; already on another call")
|
|
}
|
|
return
|
|
}
|
|
callService.initiateCall(to: callTarget, isVideo: isVideo)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// MARK: - Events
|
|
|
|
@objc
|
|
private func registrationStateDidChange() {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
scheduleBgAppRefresh()
|
|
|
|
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
|
let isRegistered = tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered
|
|
if isRegistered {
|
|
appReadiness.runNowOrWhenAppDidBecomeReadySync {
|
|
SSKEnvironment.shared.databaseStorageRef.write { transaction in
|
|
let localAddress = tsAccountManager.localIdentifiers(tx: transaction.asV2Read)?.aciAddress
|
|
Logger.info("localAddress: \(String(describing: localAddress))")
|
|
|
|
ExperienceUpgradeFinder.markAllCompleteForNewUser(transaction: transaction.unwrapGrdbWrite)
|
|
}
|
|
}
|
|
DependenciesBridge.shared.attachmentDownloadManager.beginDownloadingIfNecessary()
|
|
Task {
|
|
try await StickerManager.downloadPendingSickerPacks()
|
|
}
|
|
}
|
|
|
|
Self.updateApplicationShortcutItems(isRegistered: isRegistered)
|
|
}
|
|
|
|
// MARK: - Shortcut Items
|
|
|
|
func application(
|
|
_ application: UIApplication,
|
|
performActionFor shortcutItem: UIApplicationShortcutItem,
|
|
completionHandler: @escaping (Bool) -> Void
|
|
) {
|
|
AssertIsOnMainThread()
|
|
|
|
if didAppLaunchFail {
|
|
completionHandler(false)
|
|
return
|
|
}
|
|
|
|
appReadiness.runNowOrWhenUIDidBecomeReadySync {
|
|
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
|
guard tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
|
|
let controller = ActionSheetController(
|
|
title: OWSLocalizedString("REGISTER_CONTACTS_WELCOME", comment: ""),
|
|
message: OWSLocalizedString("REGISTRATION_RESTRICTED_MESSAGE", comment: "")
|
|
)
|
|
controller.addAction(ActionSheetAction(title: CommonStrings.okButton))
|
|
UIApplication.shared.frontmostViewController?.present(controller, animated: true, completion: {
|
|
completionHandler(false)
|
|
})
|
|
return
|
|
}
|
|
SignalApp.shared.showNewConversationView()
|
|
completionHandler(true)
|
|
}
|
|
}
|
|
|
|
public static func updateApplicationShortcutItems(isRegistered: Bool) {
|
|
guard CurrentAppContext().isMainApp else { return }
|
|
UIApplication.shared.shortcutItems = applicationShortcutItems(isRegistered: isRegistered)
|
|
}
|
|
|
|
static func applicationShortcutItems(isRegistered: Bool) -> [UIApplicationShortcutItem] {
|
|
guard isRegistered else { return [] }
|
|
return [.init(
|
|
type: "\(Bundle.main.bundleIdPrefix).quickCompose",
|
|
localizedTitle: OWSLocalizedString(
|
|
"APPLICATION_SHORTCUT_NEW_MESSAGE",
|
|
comment: "On the iOS home screen, if you tap and hold the Signal icon, this shortcut will appear. Tapping it will let users send a new message. You may want to refer to similar behavior in other iOS apps, such as Messages, for equivalent strings."
|
|
),
|
|
localizedSubtitle: nil,
|
|
icon: UIApplicationShortcutIcon(type: .compose)
|
|
)]
|
|
}
|
|
|
|
// MARK: - URL Handling
|
|
|
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
|
AssertIsOnMainThread()
|
|
return handleOpenUrl(url)
|
|
}
|
|
|
|
private func handleOpenUrl(_ url: URL) -> Bool {
|
|
AssertIsOnMainThread()
|
|
|
|
if didAppLaunchFail {
|
|
return false
|
|
}
|
|
|
|
guard let parsedUrl = UrlOpener.parseUrl(url) else {
|
|
return false
|
|
}
|
|
let appReadiness: AppReadinessSetter = self.appReadiness
|
|
appReadiness.runNowOrWhenUIDidBecomeReadySync {
|
|
let urlOpener = UrlOpener(
|
|
appReadiness: appReadiness,
|
|
databaseStorage: SSKEnvironment.shared.databaseStorageRef,
|
|
tsAccountManager: DependenciesBridge.shared.tsAccountManager
|
|
)
|
|
|
|
urlOpener.openUrl(parsedUrl, in: self.window!)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// MARK: - Database integrity checks
|
|
|
|
private func checkDatabaseIntegrityIfNecessary(
|
|
isRegistered: Bool
|
|
) {
|
|
guard isRegistered, FeatureFlags.periodicallyCheckDatabaseIntegrity else { return }
|
|
|
|
let appReadiness: AppReadiness = self.appReadiness
|
|
DispatchQueue.sharedUtility.async {
|
|
switch GRDBDatabaseStorageAdapter.checkIntegrity(databaseStorage: SSKEnvironment.shared.databaseStorageRef) {
|
|
case .ok: break
|
|
case .notOk:
|
|
appReadiness.runNowOrWhenUIDidBecomeReadySync {
|
|
OWSActionSheets.showActionSheet(
|
|
title: "Database corrupted!",
|
|
message: "We have detected database corruption on your device. Please submit debug logs to the iOS team."
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - UNUserNotificationCenterDelegate
|
|
|
|
extension AppDelegate: UNUserNotificationCenterDelegate {
|
|
// The method will be called on the delegate when the user responded to the notification by opening the application,
|
|
// dismissing the notification or choosing a UNNotificationAction. The delegate must be set before the application
|
|
// returns from application:didFinishLaunchingWithOptions:.
|
|
public func userNotificationCenter(
|
|
_ center: UNUserNotificationCenter,
|
|
didReceive response: UNNotificationResponse,
|
|
withCompletionHandler completionHandler: @escaping () -> Void
|
|
) {
|
|
let appReadiness: AppReadinessSetter = self.appReadiness
|
|
appReadiness.runNowOrWhenAppDidBecomeReadySync {
|
|
NotificationActionHandler.handleNotificationResponse(
|
|
response,
|
|
appReadiness: appReadiness,
|
|
completionHandler: completionHandler
|
|
)
|
|
}
|
|
}
|
|
}
|