From 5df23bdaedc1b809b78fc65bfcacdcc7b2d67b65 Mon Sep 17 00:00:00 2001 From: Harry <109690906+harry-signal@users.noreply.github.com> Date: Tue, 14 Nov 2023 11:22:50 -0800 Subject: [PATCH] Migrate to BGAppRefreshTask --- Signal.xcodeproj/project.pbxproj | 4 + Signal/Signal-Info.plist | 4 + Signal/src/AppDelegate.m | 21 ---- Signal/src/AppDelegate.swift | 22 ++-- Signal/src/MessageFetchBGRefreshTask.swift | 138 +++++++++++++++++++++ 5 files changed, 153 insertions(+), 36 deletions(-) create mode 100644 Signal/src/MessageFetchBGRefreshTask.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index ff4a12e18b..04ef8df50c 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -937,6 +937,7 @@ 66C2B1562A1400E8008DDE72 /* SVR2WebsocketConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C2B1552A1400E8008DDE72 /* SVR2WebsocketConfigurator.swift */; }; 66C336D12A994B97000F4F50 /* FailedStorySendDisplayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C336D02A994B97000F4F50 /* FailedStorySendDisplayController.swift */; }; 66C3887E29CA537400E6DC00 /* RegistrationTransferProgressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C3887D29CA537400E6DC00 /* RegistrationTransferProgressViewController.swift */; }; + 66CDB7522AF9D117009A36EC /* MessageFetchBGRefreshTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CDB7512AF9D117009A36EC /* MessageFetchBGRefreshTask.swift */; }; 66CDE5C42A58928000B7C607 /* SpoilerableTextViewAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CDE5C32A58928000B7C607 /* SpoilerableTextViewAnimator.swift */; }; 66CE755F28C332AF00D5FA79 /* PaymentOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CE755E28C332AF00D5FA79 /* PaymentOnboarding.swift */; }; 66D13F0A2A731E590092D47B /* RecipientHidingManager+SignalServiceAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D13F092A731E590092D47B /* RecipientHidingManager+SignalServiceAddress.swift */; }; @@ -3535,6 +3536,7 @@ 66C2B1552A1400E8008DDE72 /* SVR2WebsocketConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVR2WebsocketConfigurator.swift; sourceTree = ""; }; 66C336D02A994B97000F4F50 /* FailedStorySendDisplayController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedStorySendDisplayController.swift; sourceTree = ""; }; 66C3887D29CA537400E6DC00 /* RegistrationTransferProgressViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationTransferProgressViewController.swift; sourceTree = ""; }; + 66CDB7512AF9D117009A36EC /* MessageFetchBGRefreshTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageFetchBGRefreshTask.swift; sourceTree = ""; }; 66CDE5C32A58928000B7C607 /* SpoilerableTextViewAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpoilerableTextViewAnimator.swift; sourceTree = ""; }; 66CE755E28C332AF00D5FA79 /* PaymentOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentOnboarding.swift; sourceTree = ""; }; 66D13F092A731E590092D47B /* RecipientHidingManager+SignalServiceAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RecipientHidingManager+SignalServiceAddress.swift"; sourceTree = ""; }; @@ -7232,6 +7234,7 @@ 76EB03C218170B33006006FC /* AppDelegate.h */, 76EB03C318170B33006006FC /* AppDelegate.m */, F92074752888648A00B7F087 /* AppDelegate.swift */, + 66CDB7512AF9D117009A36EC /* MessageFetchBGRefreshTask.swift */, 45B201741DAECBFD00C461E0 /* Signal-Bridging-Header.h */, ); path = src; @@ -12116,6 +12119,7 @@ 4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */, 4CB5F26720F6E1E2004D1B42 /* MessageActionsToolbar.swift in Sources */, 45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */, + 66CDB7522AF9D117009A36EC /* MessageFetchBGRefreshTask.swift in Sources */, 34DE9C02256575300080E4AF /* MessageLoader.swift in Sources */, 881218F0238CA51600E6F271 /* MessageReactionPicker.swift in Sources */, 88D1D40222EBB5A100F472C5 /* MessageRequestView.swift in Sources */, diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index dd82f07609..b9c3da2d4a 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -2,6 +2,10 @@ + BGTaskSchedulerPermittedIdentifiers + + MessageFetchBGRefreshTask + BuildDetails CFBundleDevelopmentRegion diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index b9ec7a23b2..aa6d69700e 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -448,25 +448,4 @@ static void uncaughtExceptionHandler(NSException *exception) }]; } -- (void)application:(UIApplication *)application - performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler -{ - OWSLogInfo(@"performing background fetch"); - AppReadinessRunNowOrWhenAppDidBecomeReadySync(^{ - [self.messageFetcherJob runObjc].done(^(id value) { - // HACK: Call completion handler after n seconds. - // - // We don't currently have a convenient API to know when message fetching is *done* when - // working with the websocket. - // - // We *could* substantially rewrite the SocketManager to take advantage of the `empty` message - // But once our REST endpoint is fixed to properly de-enqueue fallback notifications, we can easily - // use the rest endpoint here rather than the websocket and circumvent making changes to critical code. - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - completionHandler(UIBackgroundFetchResultNewData); - }); - }); - }); -} - @end diff --git a/Signal/src/AppDelegate.swift b/Signal/src/AppDelegate.swift index 895ccc0c0f..1bae6f3a52 100644 --- a/Signal/src/AppDelegate.swift +++ b/Signal/src/AppDelegate.swift @@ -77,6 +77,8 @@ extension AppDelegate { Cryptography.seedRandom() + MessageFetchBGRefreshTask.register() + // 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 @@ -377,7 +379,7 @@ extension AppDelegate { DebugLogger.shared().postLaunchLogCleanup(appContext: appContext) AppVersionImpl.shared.mainAppLaunchDidComplete() - enableBackgroundRefreshIfNecessary() + scheduleBgAppRefresh() Self.updateApplicationShortcutItems(isRegistered: tsRegistrationState.isRegistered) let notificationCenter = NotificationCenter.default @@ -412,18 +414,8 @@ extension AppDelegate { SignalApp.shared.showLaunchInterface(launchInterface, launchStartedAt: launchStartedAt) } - private func enableBackgroundRefreshIfNecessary() { - let interval: TimeInterval - if - OWS2FAManager.shared.isRegistrationLockEnabled, - DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered - { - // Ping server once a day to keep-alive reglock clients. - interval = 24 * 60 * 60 - } else { - interval = UIApplication.backgroundFetchIntervalNever - } - UIApplication.shared.setMinimumBackgroundFetchInterval(interval) + private func scheduleBgAppRefresh() { + MessageFetchBGRefreshTask.shared?.scheduleTask() } /// The user must unlock the device once after reboot before the database encryption key can be accessed. @@ -861,7 +853,7 @@ extension AppDelegate { Logger.info("registrationStateDidChange") - enableBackgroundRefreshIfNecessary() + scheduleBgAppRefresh() let tsAccountManager = DependenciesBridge.shared.tsAccountManager let isRegistered = tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered @@ -885,7 +877,7 @@ extension AppDelegate { @objc private func registrationLockDidChange() { - enableBackgroundRefreshIfNecessary() + scheduleBgAppRefresh() } // MARK: - Utilities diff --git a/Signal/src/MessageFetchBGRefreshTask.swift b/Signal/src/MessageFetchBGRefreshTask.swift new file mode 100644 index 0000000000..5882e96053 --- /dev/null +++ b/Signal/src/MessageFetchBGRefreshTask.swift @@ -0,0 +1,138 @@ +// +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import BackgroundTasks +import SignalServiceKit + +/** + * Utility class for managing the BGAppRefreshTask we use as a "keepalive" for + * registration lock. + * + * Ensures that while reglock is active, we try to fetch messages every once in a while + * even if the app or NSE don't launch, so that the server keeps the account active + * and reglock alive. + */ +public class MessageFetchBGRefreshTask { + + private static var _shared: MessageFetchBGRefreshTask = { + return MessageFetchBGRefreshTask( + dateProvider: { Date() }, + messageFetcherJob: NSObject.messageFetcherJob, + ows2FAManager: NSObject.ows2FAManager, + tsAccountManager: DependenciesBridge.shared.tsAccountManager + ) + }() + + public static var shared: MessageFetchBGRefreshTask? { + guard AppReadiness.isAppReady else { + return nil + } + return _shared + } + + // Must be kept in sync with the value in info.plist. + private static let taskIdentifier = "MessageFetchBGRefreshTask" + + private let dateProvider: DateProvider + private let messageFetcherJob: MessageFetcherJob + private let ows2FAManager: OWS2FAManager + private let tsAccountManager: TSAccountManager + + private init( + dateProvider: @escaping DateProvider, + messageFetcherJob: MessageFetcherJob, + ows2FAManager: OWS2FAManager, + tsAccountManager: TSAccountManager + ) { + self.dateProvider = dateProvider + self.messageFetcherJob = messageFetcherJob + self.ows2FAManager = ows2FAManager + self.tsAccountManager = tsAccountManager + } + + public static func register() { + BGTaskScheduler.shared.register( + forTaskWithIdentifier: Self.taskIdentifier, + using: nil, + launchHandler: { task in + AppReadiness.runNowOrWhenAppDidBecomeReadyAsync { + Self.shared?.performTask(task) + } + } + ) + } + + public func scheduleTask() { + // Note: this file only exists in the main app (Signal/src) so we + // don't check for that. But if this ever moves, it should check + // appContext.isMainApp. + + guard ows2FAManager.isRegistrationLockEnabled else { + // No need to do the keepalive for reglock. + return + } + + guard tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else { + return + } + + // Ping server once a day to keep-alive reglock clients. + // Ideally, we would schedule this for 24 hours _since we last talked to the chat server_. + // Without knowing that, we risk scheduling this 24 hours out over and over every time you + // launch the app without internet. That scenario is unlikely, so is left unhandled. + let interval: TimeInterval = 24 * 60 * 60 + let request = BGAppRefreshTaskRequest(identifier: Self.taskIdentifier) + request.earliestBeginDate = dateProvider().addingTimeInterval(interval) + + do { + try BGTaskScheduler.shared.submit(request) + } catch let error { + let errorCode = (error as NSError).code + switch errorCode { + case BGTaskScheduler.Error.Code.notPermitted.rawValue: + Logger.warn("Skipping bg task; user permission required.") + case BGTaskScheduler.Error.Code.tooManyPendingTaskRequests.rawValue: + // If we reschedule the same identifier, we don't get this error. + // This means a task with a different identifier was scheduled (not allowed). + Logger.error("Too many pending bg tasks; only one app refresh task identifier is allowed at any time.") + case BGTaskScheduler.Error.Code.unavailable.rawValue: + Logger.error("Trying to schedule bg task from an extension?") + default: + Logger.error("Unknown error code scheduling bg task: \(errorCode)") + } + } + } + + private func performTask(_ task: BGTask) { + Logger.info("performing background fetch") + AppReadiness.runNowOrWhenUIDidBecomeReadySync { + self.messageFetcherJob.run().promise + .timeout(seconds: 10) + .then { + // HACK: Call completion handler after 5 seconds. + // + // We don't currently have a convenient API to know when message fetching is *done* when + // working with the websocket. + // + // We *could* substantially rewrite the SocketManager to take advantage of the `empty` message + // But once our REST endpoint is fixed to properly de-enqueue fallback notifications, we can easily + // use the rest endpoint here rather than the websocket and circumvent making changes to critical code. + return Guarantee.after(seconds: 5) + } + .observe { result in + switch result { + case .success: + Logger.info("success") + task.setTaskCompleted(success: true) + case .failure: + Logger.error("Failing task; failed to fetch messages") + task.setTaskCompleted(success: false) + } + // Schedule the next run now. + self.scheduleTask() + } + } + } +}