244 lines
8.9 KiB
Swift
244 lines
8.9 KiB
Swift
//
|
|
// Copyright 2025 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import SignalServiceKit
|
|
import SwiftUI
|
|
|
|
final class BackupSettingsAttachmentUploadTracker {
|
|
struct UploadUpdate: Equatable {
|
|
enum State {
|
|
case running
|
|
case pausedLowBattery
|
|
case pausedLowPowerMode
|
|
case pausedNeedsWifi
|
|
case pausedNeedsInternet
|
|
}
|
|
|
|
let state: State
|
|
var bytesUploaded: UInt64 { progress.completedUnitCount }
|
|
var totalBytesToUpload: UInt64 { progress.totalUnitCount }
|
|
var percentageUploaded: Float { progress.percentComplete }
|
|
|
|
private let progress: OWSProgress
|
|
|
|
init(state: State, bytesUploaded: UInt64, totalBytesToUpload: UInt64) {
|
|
self.init(state: state, progress: OWSProgress(
|
|
completedUnitCount: bytesUploaded,
|
|
totalUnitCount: totalBytesToUpload,
|
|
))
|
|
}
|
|
|
|
fileprivate init(state: State, progress: OWSProgress) {
|
|
self.state = state
|
|
self.progress = progress
|
|
}
|
|
|
|
static func == (lhs: UploadUpdate, rhs: UploadUpdate) -> Bool {
|
|
return lhs.state == rhs.state && lhs.percentageUploaded == rhs.percentageUploaded
|
|
}
|
|
}
|
|
|
|
private let backupAttachmentUploadQueueStatusReporter: BackupAttachmentUploadQueueStatusReporter
|
|
private let backupAttachmentUploadProgress: BackupAttachmentUploadProgress
|
|
|
|
init(
|
|
backupAttachmentUploadQueueStatusReporter: BackupAttachmentUploadQueueStatusReporter,
|
|
backupAttachmentUploadProgress: BackupAttachmentUploadProgress,
|
|
) {
|
|
self.backupAttachmentUploadQueueStatusReporter = backupAttachmentUploadQueueStatusReporter
|
|
self.backupAttachmentUploadProgress = backupAttachmentUploadProgress
|
|
}
|
|
|
|
func updates() -> AsyncStream<UploadUpdate?> {
|
|
return AsyncStream { continuation in
|
|
let tracker = Tracker(
|
|
backupAttachmentUploadQueueStatusReporter: backupAttachmentUploadQueueStatusReporter,
|
|
backupAttachmentUploadProgress: backupAttachmentUploadProgress,
|
|
continuation: continuation
|
|
)
|
|
|
|
tracker.start()
|
|
|
|
continuation.onTermination = { reason in
|
|
switch reason {
|
|
case .cancelled:
|
|
tracker.stop()
|
|
case .finished:
|
|
owsFailDebug("How did we finish? We should've canceled first.")
|
|
@unknown default:
|
|
owsFailDebug("Unexpected continuation termination reason: \(reason)")
|
|
tracker.stop()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private class Tracker {
|
|
typealias UploadUpdate = BackupSettingsAttachmentUploadTracker.UploadUpdate
|
|
|
|
private struct State {
|
|
var lastReportedUploadProgress: OWSProgress = .zero
|
|
var lastReportedUploadQueueStatus: BackupAttachmentUploadQueueStatus?
|
|
|
|
var uploadQueueStatusObserver: NotificationCenter.Observer?
|
|
var uploadProgressObserver: BackupAttachmentUploadProgress.Observer?
|
|
|
|
let streamContinuation: AsyncStream<UploadUpdate?>.Continuation
|
|
}
|
|
|
|
private let backupAttachmentUploadQueueStatusReporter: BackupAttachmentUploadQueueStatusReporter
|
|
private let backupAttachmentUploadProgress: BackupAttachmentUploadProgress
|
|
private let state: SeriallyAccessedState<State>
|
|
|
|
init(
|
|
backupAttachmentUploadQueueStatusReporter: BackupAttachmentUploadQueueStatusReporter,
|
|
backupAttachmentUploadProgress: BackupAttachmentUploadProgress,
|
|
continuation: AsyncStream<UploadUpdate?>.Continuation
|
|
) {
|
|
self.backupAttachmentUploadQueueStatusReporter = backupAttachmentUploadQueueStatusReporter
|
|
self.backupAttachmentUploadProgress = backupAttachmentUploadProgress
|
|
self.state = SeriallyAccessedState(State(
|
|
streamContinuation: continuation
|
|
))
|
|
}
|
|
|
|
func start() {
|
|
state.enqueueUpdate { @MainActor [self] _state in
|
|
_state.uploadQueueStatusObserver = observeUploadQueueStatus()
|
|
}
|
|
}
|
|
|
|
func stop() {
|
|
state.enqueueUpdate { [self] _state in
|
|
if let uploadQueueStatusObserver = _state.uploadQueueStatusObserver {
|
|
NotificationCenter.default.removeObserver(uploadQueueStatusObserver)
|
|
}
|
|
|
|
if let uploadProgressObserver = _state.uploadProgressObserver {
|
|
await backupAttachmentUploadProgress.removeObserver(uploadProgressObserver)
|
|
}
|
|
|
|
_state.streamContinuation.finish()
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@MainActor
|
|
private func observeUploadQueueStatus() -> NotificationCenter.Observer {
|
|
// We only care about fullsize uploads, ignore thumbnails
|
|
let uploadQueueStatusObserver = NotificationCenter.default.addObserver(
|
|
name: .backupAttachmentUploadQueueStatusDidChange(for: .fullsize)
|
|
) { [weak self] notification in
|
|
guard let self else { return }
|
|
|
|
handleQueueStatusUpdate(
|
|
backupAttachmentUploadQueueStatusReporter.currentStatus(for: .fullsize)
|
|
)
|
|
}
|
|
|
|
// Now that we're observing updates, handle the initial value as if we'd
|
|
// just gotten it in an update.
|
|
handleQueueStatusUpdate(
|
|
backupAttachmentUploadQueueStatusReporter.currentStatus(for: .fullsize)
|
|
)
|
|
|
|
return uploadQueueStatusObserver
|
|
}
|
|
|
|
private func handleQueueStatusUpdate(
|
|
_ queueStatus: BackupAttachmentUploadQueueStatus,
|
|
) {
|
|
state.enqueueUpdate { [self] _state in
|
|
_state.lastReportedUploadQueueStatus = queueStatus
|
|
|
|
switch queueStatus {
|
|
case .empty:
|
|
yieldCurrentUploadUpdate(state: _state)
|
|
case
|
|
.running,
|
|
.noWifiReachability, .lowBattery, .lowPowerMode, .noReachability,
|
|
.notRegisteredAndReady, .appBackgrounded, .suspended, .hasConsumedMediaTierCapacity:
|
|
// The queue isn't empty, so attach a new progress observer.
|
|
//
|
|
// Progress observers snapshot and filter the queue's state, so
|
|
// any time the queue is non-empty we want to make sure we have
|
|
// an observer with a filtered-snapshot of the latest state.
|
|
//
|
|
// For example, when we first enable paid-tier Backups the queue
|
|
// starts empty and is populated when we run list-media for the
|
|
// first time.
|
|
//
|
|
// The observer we attach will yield an update, so we don't need
|
|
// to here.
|
|
if let existingObserver = _state.uploadProgressObserver {
|
|
await backupAttachmentUploadProgress.removeObserver(existingObserver)
|
|
}
|
|
|
|
_state.uploadProgressObserver = try? await backupAttachmentUploadProgress
|
|
.addObserver { [weak self] progressUpdate in
|
|
guard let self else { return }
|
|
handleUploadProgressUpdate(progressUpdate)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handleUploadProgressUpdate(_ uploadProgress: OWSProgress) {
|
|
state.enqueueUpdate { [self] _state in
|
|
_state.lastReportedUploadProgress = uploadProgress
|
|
yieldCurrentUploadUpdate(state: _state)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private func yieldCurrentUploadUpdate(state: State) {
|
|
let streamContinuation = state.streamContinuation
|
|
let lastReportedUploadProgress = state.lastReportedUploadProgress
|
|
|
|
guard let lastReportedUploadQueueStatus = state.lastReportedUploadQueueStatus else {
|
|
return
|
|
}
|
|
|
|
guard lastReportedUploadProgress.totalUnitCount > 0 else {
|
|
// We have no meaningful progress to report on.
|
|
return
|
|
}
|
|
|
|
let uploadUpdateState: UploadUpdate.State? = {
|
|
switch lastReportedUploadQueueStatus {
|
|
case .empty:
|
|
return nil
|
|
case .notRegisteredAndReady, .appBackgrounded, .suspended:
|
|
return nil
|
|
case .running:
|
|
return .running
|
|
case .noReachability:
|
|
return .pausedNeedsInternet
|
|
case .noWifiReachability:
|
|
return .pausedNeedsWifi
|
|
case .lowBattery:
|
|
return .pausedLowBattery
|
|
case .lowPowerMode:
|
|
return .pausedLowPowerMode
|
|
case .hasConsumedMediaTierCapacity:
|
|
// This gets bubbled up via other mechanisms; to the UI
|
|
// this upload state doesn't show a bar so its nil.
|
|
return nil
|
|
}
|
|
}()
|
|
|
|
if let uploadUpdateState {
|
|
streamContinuation.yield(UploadUpdate(state: uploadUpdateState, progress: lastReportedUploadProgress))
|
|
} else {
|
|
streamContinuation.yield(nil)
|
|
}
|
|
}
|
|
}
|