session-ios/Session/Notifications/SyncPushTokensJob.swift

215 lines
8.5 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import GRDB
import SignalCoreKit
import SessionMessagingKit
import SessionUtilitiesKit
import SignalCoreKit
public enum SyncPushTokensJob: JobExecutor {
public static let maxFailureCount: Int = -1
public static let requiresThreadId: Bool = false
public static let requiresInteractionId: Bool = false
private static let maxFrequency: TimeInterval = (12 * 60 * 60)
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
) {
// Don't run when inactive or not in main app or if the user doesn't exist yet
guard (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) else {
return deferred(job) // Don't need to do anything if it's not the main app
}
guard Identity.userCompletedRequiredOnboarding() else {
SNLog("[SyncPushTokensJob] Deferred due to incomplete registration")
return deferred(job)
}
// We need to check a UIApplication setting which needs to run on the main thread so synchronously
// retrieve the value so we can continue
let isRegisteredForRemoteNotifications: Bool = {
guard !Thread.isMainThread else {
return UIApplication.shared.isRegisteredForRemoteNotifications
}
return DispatchQueue.main.sync {
return UIApplication.shared.isRegisteredForRemoteNotifications
}
}()
// Push tokens don't normally change while the app is launched, so you would assume checking once
// during launch is 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.
//
// It's also possible for a device to successfully register for push notifications but fail to
// register with Session
//
// Due to the above we want to re-register at least once every ~12 hours to ensure users will
// continue to receive push notifications
//
// In addition to this if we are custom running the job (eg. by toggling the push notification
// setting) then we should run regardless of the other settings so users have a mechanism to force
// the registration to run
let lastPushNotificationSync: Date = UserDefaults.standard[.lastPushNotificationSync]
.defaulting(to: Date.distantPast)
guard
job.behaviour == .runOnce ||
!isRegisteredForRemoteNotifications ||
Date().timeIntervalSince(lastPushNotificationSync) >= SyncPushTokensJob.maxFrequency
else {
SNLog("[SyncPushTokensJob] Deferred due to Fast Mode disabled or recent-enough registration")
deferred(job) // Don't need to do anything if push notifications are already registered
return
}
Logger.info("Re-registering for remote notifications.")
// Perform device registration
PushRegistrationManager.shared.requestPushTokens()
.subscribe(on: queue)
.flatMap { (pushToken: String, voipToken: String) -> AnyPublisher<Void, Error> in
Deferred {
Future<Void, Error> { resolver in
SyncPushTokensJob.registerForPushNotifications(
pushToken: pushToken,
voipToken: voipToken,
isForcedUpdate: true,
success: { resolver(Result.success(())) },
failure: { resolver(Result.failure($0)) }
)
}
}
.handleEvents(
receiveCompletion: { result in
switch result {
case .failure(let error):
SNLog("[SyncPushTokensJob] Failed to register due to error: \(error)")
case .finished:
Logger.warn("Recording push tokens locally. pushToken: \(redact(pushToken)), voipToken: \(redact(voipToken))")
SNLog("[SyncPushTokensJob] Completed")
UserDefaults.standard[.lastPushNotificationSync] = Date()
Storage.shared.write { db in
db[.lastRecordedPushToken] = pushToken
db[.lastRecordedVoipToken] = voipToken
}
}
}
)
.eraseToAnyPublisher()
}
.sinkUntilComplete(
// We want to complete this job regardless of success or failure
receiveCompletion: { _ in success(job, false) },
receiveValue: { _ in }
)
}
public static func run(uploadOnlyIfStale: Bool) {
guard let job: Job = Job(
variant: .syncPushTokens,
behaviour: .runOnce,
details: SyncPushTokensJob.Details(
uploadOnlyIfStale: uploadOnlyIfStale
)
)
else { return }
SyncPushTokensJob.run(
job,
queue: DispatchQueue.global(qos: .default),
success: { _, _ in },
failure: { _, _, _ in },
deferred: { _ in }
)
}
}
// MARK: - SyncPushTokensJob.Details
extension SyncPushTokensJob {
public struct Details: Codable {
public let uploadOnlyIfStale: Bool
}
}
// MARK: - Convenience
private func redact(_ string: String) -> String {
return OWSIsDebugBuild() ? string : "[ READACTED \(string.prefix(2))...\(string.suffix(2)) ]"
}
extension SyncPushTokensJob {
fileprivate static func registerForPushNotifications(
pushToken: String,
voipToken: String,
isForcedUpdate: Bool,
success: @escaping () -> (),
failure: @escaping (Error) -> (),
remainingRetries: Int = 3
) {
let isUsingFullAPNs: Bool = UserDefaults.standard[.isUsingFullAPNs]
Just(Data(hex: pushToken))
.setFailureType(to: Error.self)
.flatMap { pushTokenAsData -> AnyPublisher<Bool, Error> in
guard isUsingFullAPNs else {
return PushNotificationAPI.unregister(pushTokenAsData)
.map { _ in true }
.eraseToAnyPublisher()
}
return PushNotificationAPI
.register(
with: pushTokenAsData,
publicKey: getUserHexEncodedPublicKey(),
isForcedUpdate: isForcedUpdate
)
.map { _ in true }
.eraseToAnyPublisher()
}
.catch { error -> AnyPublisher<Bool, Error> in
guard remainingRetries == 0 else {
SyncPushTokensJob.registerForPushNotifications(
pushToken: pushToken,
voipToken: voipToken,
isForcedUpdate: isForcedUpdate,
success: success,
failure: failure,
remainingRetries: (remainingRetries - 1)
)
return Just(false)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
return Fail(error: error)
.eraseToAnyPublisher()
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete(
receiveCompletion: { result in
switch result {
case .finished: break
case .failure(let error): failure(error)
}
},
receiveValue: { didComplete in
guard didComplete else { return }
success()
}
)
}
}