244 lines
9.4 KiB
Swift
244 lines
9.4 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
|
|
public class AccountAttributesUpdaterImpl: AccountAttributesUpdater {
|
|
|
|
private let appReadiness: Shims.AppReadiness
|
|
private let appVersion: AppVersion
|
|
private let dateProvider: DateProvider
|
|
private let db: DB
|
|
private let profileManager: ProfileManager
|
|
private let serviceClient: SignalServiceClient
|
|
private let schedulers: Schedulers
|
|
private let svrLocalStorage: SVRLocalStorage
|
|
private let syncManager: SyncManagerProtocol
|
|
private let tsAccountManager: TSAccountManager
|
|
|
|
private let kvStore: KeyValueStore
|
|
|
|
public init(
|
|
appReadiness: Shims.AppReadiness,
|
|
appVersion: AppVersion,
|
|
dateProvider: @escaping DateProvider,
|
|
db: DB,
|
|
profileManager: ProfileManager,
|
|
keyValueStoreFactory: KeyValueStoreFactory,
|
|
serviceClient: SignalServiceClient,
|
|
schedulers: Schedulers,
|
|
svrLocalStorage: SVRLocalStorage,
|
|
syncManager: SyncManagerProtocol,
|
|
tsAccountManager: TSAccountManager
|
|
) {
|
|
self.appReadiness = appReadiness
|
|
self.appVersion = appVersion
|
|
self.dateProvider = dateProvider
|
|
self.db = db
|
|
self.profileManager = profileManager
|
|
self.serviceClient = serviceClient
|
|
self.schedulers = schedulers
|
|
self.svrLocalStorage = svrLocalStorage
|
|
self.syncManager = syncManager
|
|
self.tsAccountManager = tsAccountManager
|
|
|
|
self.kvStore = keyValueStoreFactory.keyValueStore(collection: "AccountAttributesUpdater")
|
|
|
|
appReadiness.runNowOrWhenAppBecomesReadyAsync {
|
|
Task {
|
|
try await self.updateAccountAttributesIfNecessaryAttempt(authedAccount: .implicit())
|
|
}
|
|
}
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(reachabilityDidChange),
|
|
name: .reachabilityChanged,
|
|
object: nil
|
|
)
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
@objc
|
|
private func reachabilityDidChange() {
|
|
appReadiness.runNowOrWhenAppBecomesReadyAsync {
|
|
Task {
|
|
try await self.updateAccountAttributesIfNecessaryAttempt(authedAccount: .implicit())
|
|
}
|
|
}
|
|
}
|
|
|
|
public func updateAccountAttributes(authedAccount: AuthedAccount) async throws {
|
|
await db.awaitableWrite { tx in
|
|
self.kvStore.setDate(self.dateProvider(), key: Keys.latestUpdateRequestDate, transaction: tx)
|
|
}
|
|
try await self.updateAccountAttributesIfNecessaryAttempt(authedAccount: authedAccount)
|
|
}
|
|
|
|
public func scheduleAccountAttributesUpdate(authedAccount: AuthedAccount, tx: DBWriteTransaction) {
|
|
self.kvStore.setDate(self.dateProvider(), key: Keys.latestUpdateRequestDate, transaction: tx)
|
|
tx.addAsyncCompletion(on: schedulers.global()) {
|
|
Task {
|
|
try? await self.updateAccountAttributesIfNecessaryAttempt(authedAccount: authedAccount)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Performs a single attempt to update the account attributes.
|
|
//
|
|
// We need to update our account attributes in a variety of scenarios:
|
|
//
|
|
// * Every time the user upgrades to a new version.
|
|
// * Whenever the device capabilities change.
|
|
// This is useful during development and internal testing when
|
|
// moving between builds with different device capabilities.
|
|
// * Whenever another component of the system requests an attribute,
|
|
// update e.g. during registration, after rotating the profile key, etc.
|
|
//
|
|
// The client will retry failed attempts:
|
|
//
|
|
// * On launch.
|
|
// * When reachability changes.
|
|
private func updateAccountAttributesIfNecessaryAttempt(authedAccount: AuthedAccount) async throws {
|
|
guard appReadiness.isAppReady() else {
|
|
Logger.info("Aborting; app is not ready.")
|
|
return
|
|
}
|
|
|
|
let currentAppVersion = appVersion.currentAppVersion
|
|
|
|
enum ShouldUpdate {
|
|
case no
|
|
case yes(
|
|
currentDeviceCapabilities: AccountAttributes.Capabilities,
|
|
lastAttributeRequestDate: Date?,
|
|
registrationState: TSRegistrationState
|
|
)
|
|
}
|
|
|
|
let shouldUpdate = db.read { tx -> ShouldUpdate in
|
|
let registrationState = self.tsAccountManager.registrationState(tx: tx)
|
|
let isRegistered = registrationState.isRegistered
|
|
|
|
guard isRegistered else {
|
|
return .no
|
|
}
|
|
|
|
// has non-nil value if isRegistered is true.
|
|
let hasBackedUpMasterKey = self.svrLocalStorage.getIsMasterKeyBackedUp(tx)
|
|
let currentDeviceCapabilities = AccountAttributes.Capabilities(hasSVRBackups: hasBackedUpMasterKey)
|
|
|
|
// Check if there's been a request for an attributes update.
|
|
let lastAttributeRequestDate = self.kvStore.getDate(Keys.latestUpdateRequestDate, transaction: tx)
|
|
if lastAttributeRequestDate != nil {
|
|
return .yes(
|
|
currentDeviceCapabilities: currentDeviceCapabilities,
|
|
lastAttributeRequestDate: lastAttributeRequestDate,
|
|
registrationState: registrationState
|
|
)
|
|
}
|
|
|
|
// Check if device capabilities have changed.
|
|
let lastUpdateDeviceCapabilities = self.kvStore.getObject(
|
|
forKey: Keys.lastUpdateDeviceCapabilities,
|
|
transaction: tx
|
|
) as? [String: NSNumber]
|
|
if lastUpdateDeviceCapabilities != currentDeviceCapabilities.requestParameters {
|
|
return .yes(
|
|
currentDeviceCapabilities: currentDeviceCapabilities,
|
|
lastAttributeRequestDate: lastAttributeRequestDate,
|
|
registrationState: registrationState
|
|
)
|
|
}
|
|
// Check if the app version has changed.
|
|
let lastUpdateAppVersion = self.kvStore.getString(Keys.lastUpdateAppVersion, transaction: tx)
|
|
if lastUpdateAppVersion != currentAppVersion {
|
|
return .yes(
|
|
currentDeviceCapabilities: currentDeviceCapabilities,
|
|
lastAttributeRequestDate: lastAttributeRequestDate,
|
|
registrationState: registrationState
|
|
)
|
|
}
|
|
return .no
|
|
}
|
|
switch shouldUpdate {
|
|
case .no:
|
|
return
|
|
case let .yes(currentDeviceCapabilities, lastAttributeRequestDate, registrationState):
|
|
Logger.info("Updating account attributes.")
|
|
let reportedDeviceCapabilities: AccountAttributes.Capabilities
|
|
if registrationState.isPrimaryDevice == true {
|
|
let attributes = try await serviceClient.updatePrimaryDeviceAccountAttributes(authedAccount: authedAccount)
|
|
reportedDeviceCapabilities = attributes.capabilities
|
|
} else {
|
|
try await serviceClient.updateSecondaryDeviceCapabilities(currentDeviceCapabilities, authedAccount: authedAccount)
|
|
reportedDeviceCapabilities = currentDeviceCapabilities
|
|
}
|
|
|
|
// Kick off an async profile fetch (not awaited)
|
|
_ = profileManager.fetchLocalUsersProfile(mainAppOnly: true, authedAccount: authedAccount)
|
|
|
|
await db.awaitableWrite { tx in
|
|
self.kvStore.setString(currentAppVersion, key: Keys.lastUpdateAppVersion, transaction: tx)
|
|
self.kvStore.setObject(reportedDeviceCapabilities.requestParameters, key: Keys.lastUpdateDeviceCapabilities, transaction: tx)
|
|
// Clear the update request unless a new update has been requested
|
|
// while this update was in flight.
|
|
if
|
|
let lastAttributeRequestDate,
|
|
lastAttributeRequestDate == self.kvStore.getDate(Keys.latestUpdateRequestDate, transaction: tx)
|
|
{
|
|
self.kvStore.removeValue(forKey: Keys.latestUpdateRequestDate, transaction: tx)
|
|
}
|
|
}
|
|
|
|
// Primary devices should sync their configuration whenever they
|
|
// update their account attributes.
|
|
if registrationState.isRegisteredPrimaryDevice {
|
|
self.syncManager.sendConfigurationSyncMessage()
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum Keys {
|
|
static let latestUpdateRequestDate = "latestUpdateRequestDate"
|
|
static let lastUpdateDeviceCapabilities = "lastUpdateDeviceCapabilities"
|
|
static let lastUpdateAppVersion = "lastUpdateAppVersion"
|
|
|
|
}
|
|
}
|
|
|
|
extension AccountAttributesUpdaterImpl {
|
|
public enum Shims {
|
|
public typealias AppReadiness = _AccountAttributesUpdater_AppReadinessShim
|
|
}
|
|
|
|
public enum Wrappers {
|
|
public typealias AppReadiness = _AccountAttributesUpdater_AppReadinessWrapper
|
|
}
|
|
}
|
|
|
|
public protocol _AccountAttributesUpdater_AppReadinessShim {
|
|
|
|
func isAppReady() -> Bool
|
|
|
|
func runNowOrWhenAppBecomesReadyAsync(_ block: @escaping () -> Void)
|
|
}
|
|
|
|
public class _AccountAttributesUpdater_AppReadinessWrapper: _AccountAttributesUpdater_AppReadinessShim {
|
|
|
|
public init() {}
|
|
|
|
public func isAppReady() -> Bool {
|
|
return AppReadiness.isAppReady
|
|
}
|
|
|
|
public func runNowOrWhenAppBecomesReadyAsync(_ block: @escaping () -> Void) {
|
|
AppReadiness.runNowOrWhenAppDidBecomeReadyAsync(block)
|
|
}
|
|
}
|