Implement interactions, handle edge cases in CLVBackupProgressView
This commit is contained in:
parent
696d33c750
commit
6ae49c28f1
@ -58,9 +58,11 @@ public class AppEnvironment: NSObject {
|
||||
let authCredentialStore = AuthCredentialStore()
|
||||
let backupAttachmentUploadEraStore = BackupAttachmentUploadEraStore()
|
||||
let backupCDNCredentialStore = BackupCDNCredentialStore()
|
||||
let backupNonceStore = BackupNonceMetadataStore()
|
||||
let backupExportJobStore = BackupExportJobStore()
|
||||
let backupSettingsStore = BackupSettingsStore()
|
||||
let backupNonceStore = BackupNonceMetadataStore()
|
||||
let backupSubscriptionIssueStore = BackupSubscriptionIssueStore()
|
||||
let clvBackupProgressViewStore = CLVBackupProgressView.Store()
|
||||
|
||||
let badgeManager = BadgeManager(
|
||||
badgeCountFetcher: DependenciesBridge.shared.badgeCountFetcher,
|
||||
@ -76,11 +78,11 @@ public class AppEnvironment: NSObject {
|
||||
db: DependenciesBridge.shared.db,
|
||||
)
|
||||
self.backupAttachmentDownloadTracker = BackupAttachmentDownloadTracker(
|
||||
backupAttachmentDownloadQueueStatusReporter: DependenciesBridge.shared.backupAttachmentDownloadQueueStatusManager,
|
||||
backupAttachmentDownloadQueueStatusManager: DependenciesBridge.shared.backupAttachmentDownloadQueueStatusManager,
|
||||
backupAttachmentDownloadProgress: DependenciesBridge.shared.backupAttachmentDownloadProgress,
|
||||
)
|
||||
self.backupAttachmentUploadTracker = BackupAttachmentUploadTracker(
|
||||
backupAttachmentUploadQueueStatusReporter: DependenciesBridge.shared.backupAttachmentUploadQueueStatusManager,
|
||||
backupAttachmentUploadQueueStatusManager: DependenciesBridge.shared.backupAttachmentUploadQueueStatusManager,
|
||||
backupAttachmentUploadProgress: DependenciesBridge.shared.backupAttachmentUploadProgress,
|
||||
)
|
||||
self.badgeManager = badgeManager
|
||||
@ -90,9 +92,11 @@ public class AppEnvironment: NSObject {
|
||||
backupAttachmentCoordinator: DependenciesBridge.shared.backupAttachmentCoordinator,
|
||||
backupAttachmentDownloadQueueStatusManager: DependenciesBridge.shared.backupAttachmentDownloadQueueStatusManager,
|
||||
backupCDNCredentialStore: backupCDNCredentialStore,
|
||||
backupExportJobStore: backupExportJobStore,
|
||||
backupKeyService: DependenciesBridge.shared.backupKeyService,
|
||||
backupPlanManager: DependenciesBridge.shared.backupPlanManager,
|
||||
backupSettingsStore: backupSettingsStore,
|
||||
clvBackupProgressViewStore: clvBackupProgressViewStore,
|
||||
db: DependenciesBridge.shared.db,
|
||||
tsAccountManager: DependenciesBridge.shared.tsAccountManager,
|
||||
)
|
||||
|
||||
@ -8,7 +8,7 @@ import SignalServiceKit
|
||||
/// Manages async streams of `DownloadUpdate`s, which represent the state and
|
||||
/// progress of Backup Attachment downloads.
|
||||
///
|
||||
/// - SeeAlso `BackupAttachmentDownloadQueueStatusReporter`
|
||||
/// - SeeAlso `BackupAttachmentDownloadQueueStatusManager`
|
||||
/// - SeeAlso `BackupAttachmentDownloadProgress`
|
||||
///
|
||||
/// - SeeAlso ``BackupAttachmentUploadTracker``
|
||||
@ -23,7 +23,6 @@ final class BackupAttachmentDownloadTracker {
|
||||
case pausedNeedsWifi
|
||||
case pausedNeedsInternet
|
||||
case outOfDiskSpace(bytesRequired: UInt64)
|
||||
case appBackgrounded
|
||||
case notRegisteredAndReady
|
||||
}
|
||||
|
||||
@ -51,21 +50,21 @@ final class BackupAttachmentDownloadTracker {
|
||||
}
|
||||
}
|
||||
|
||||
private let backupAttachmentDownloadQueueStatusReporter: BackupAttachmentDownloadQueueStatusReporter
|
||||
private let backupAttachmentDownloadQueueStatusManager: BackupAttachmentDownloadQueueStatusManager
|
||||
private let backupAttachmentDownloadProgress: BackupAttachmentDownloadProgress
|
||||
|
||||
init(
|
||||
backupAttachmentDownloadQueueStatusReporter: BackupAttachmentDownloadQueueStatusReporter,
|
||||
backupAttachmentDownloadQueueStatusManager: BackupAttachmentDownloadQueueStatusManager,
|
||||
backupAttachmentDownloadProgress: BackupAttachmentDownloadProgress,
|
||||
) {
|
||||
self.backupAttachmentDownloadQueueStatusReporter = backupAttachmentDownloadQueueStatusReporter
|
||||
self.backupAttachmentDownloadQueueStatusManager = backupAttachmentDownloadQueueStatusManager
|
||||
self.backupAttachmentDownloadProgress = backupAttachmentDownloadProgress
|
||||
}
|
||||
|
||||
func updates() -> AsyncStream<DownloadUpdate> {
|
||||
return AsyncStream { continuation in
|
||||
let tracker = Tracker(
|
||||
backupAttachmentDownloadQueueStatusReporter: backupAttachmentDownloadQueueStatusReporter,
|
||||
backupAttachmentDownloadQueueStatusManager: backupAttachmentDownloadQueueStatusManager,
|
||||
backupAttachmentDownloadProgress: backupAttachmentDownloadProgress,
|
||||
continuation: continuation,
|
||||
)
|
||||
@ -102,16 +101,16 @@ private class Tracker {
|
||||
let streamContinuation: AsyncStream<DownloadUpdate>.Continuation
|
||||
}
|
||||
|
||||
private let backupAttachmentDownloadQueueStatusReporter: BackupAttachmentDownloadQueueStatusReporter
|
||||
private let backupAttachmentDownloadQueueStatusManager: BackupAttachmentDownloadQueueStatusManager
|
||||
private let backupAttachmentDownloadProgress: BackupAttachmentDownloadProgress
|
||||
private let state: SeriallyAccessedState<State>
|
||||
|
||||
init(
|
||||
backupAttachmentDownloadQueueStatusReporter: BackupAttachmentDownloadQueueStatusReporter,
|
||||
backupAttachmentDownloadQueueStatusManager: BackupAttachmentDownloadQueueStatusManager,
|
||||
backupAttachmentDownloadProgress: BackupAttachmentDownloadProgress,
|
||||
continuation: AsyncStream<DownloadUpdate>.Continuation,
|
||||
) {
|
||||
self.backupAttachmentDownloadQueueStatusReporter = backupAttachmentDownloadQueueStatusReporter
|
||||
self.backupAttachmentDownloadQueueStatusManager = backupAttachmentDownloadQueueStatusManager
|
||||
self.backupAttachmentDownloadProgress = backupAttachmentDownloadProgress
|
||||
self.state = SeriallyAccessedState(State(streamContinuation: continuation))
|
||||
}
|
||||
@ -163,13 +162,13 @@ private class Tracker {
|
||||
|
||||
return (
|
||||
observer,
|
||||
backupAttachmentDownloadQueueStatusReporter.currentStatus(for: .fullsize),
|
||||
backupAttachmentDownloadQueueStatusManager.beginObservingIfNecessary(for: .fullsize),
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func handleDownloadQueueStatusUpdate() {
|
||||
let queueStatus = backupAttachmentDownloadQueueStatusReporter.currentStatus(for: .fullsize)
|
||||
let queueStatus = backupAttachmentDownloadQueueStatusManager.currentStatus(for: .fullsize)
|
||||
|
||||
state.enqueueUpdate { [self] _state in
|
||||
_state.lastReportedDownloadQueueStatus = queueStatus
|
||||
@ -204,33 +203,34 @@ private class Tracker {
|
||||
return
|
||||
}
|
||||
|
||||
let downloadUpdateState: DownloadUpdate.State = {
|
||||
switch lastReportedDownloadQueueStatus {
|
||||
case .empty:
|
||||
return .empty
|
||||
case .running:
|
||||
return .running
|
||||
case .suspended:
|
||||
return .suspended
|
||||
case .noWifiReachability:
|
||||
return .pausedNeedsWifi
|
||||
case .noReachability:
|
||||
return .pausedNeedsInternet
|
||||
case .lowBattery:
|
||||
return .pausedLowBattery
|
||||
case .lowPowerMode:
|
||||
return .pausedLowPowerMode
|
||||
case .lowDiskSpace:
|
||||
return .outOfDiskSpace(bytesRequired: max(
|
||||
lastReportedDownloadProgress.remainingUnitCount,
|
||||
backupAttachmentDownloadQueueStatusReporter.minimumRequiredDiskSpaceToCompleteDownloads(),
|
||||
))
|
||||
case .notRegisteredAndReady:
|
||||
return .notRegisteredAndReady
|
||||
case .appBackgrounded:
|
||||
return .appBackgrounded
|
||||
}
|
||||
}()
|
||||
let downloadUpdateState: DownloadUpdate.State
|
||||
switch lastReportedDownloadQueueStatus {
|
||||
case .appBackgrounded:
|
||||
// Don't emit an update when the app is backgrounded, so callers are
|
||||
// left with the last update before backgrounding.
|
||||
return
|
||||
case .empty:
|
||||
downloadUpdateState = .empty
|
||||
case .running:
|
||||
downloadUpdateState = .running
|
||||
case .suspended:
|
||||
downloadUpdateState = .suspended
|
||||
case .noWifiReachability:
|
||||
downloadUpdateState = .pausedNeedsWifi
|
||||
case .noReachability:
|
||||
downloadUpdateState = .pausedNeedsInternet
|
||||
case .lowBattery:
|
||||
downloadUpdateState = .pausedLowBattery
|
||||
case .lowPowerMode:
|
||||
downloadUpdateState = .pausedLowPowerMode
|
||||
case .lowDiskSpace:
|
||||
downloadUpdateState = .outOfDiskSpace(bytesRequired: max(
|
||||
lastReportedDownloadProgress.remainingUnitCount,
|
||||
backupAttachmentDownloadQueueStatusManager.minimumRequiredDiskSpaceToCompleteDownloads(),
|
||||
))
|
||||
case .notRegisteredAndReady:
|
||||
downloadUpdateState = .notRegisteredAndReady
|
||||
}
|
||||
|
||||
streamContinuation.yield(DownloadUpdate(
|
||||
state: downloadUpdateState,
|
||||
|
||||
@ -9,7 +9,7 @@ import SwiftUI
|
||||
/// Manages async streams of `UploadUpdate`s, which represent the state and
|
||||
/// progress of Backup Attachment uploads.
|
||||
///
|
||||
/// - SeeAlso `BackupAttachmentUploadQueueStatusReporter`
|
||||
/// - SeeAlso `BackupAttachmentUploadQueueStatusManager`
|
||||
/// - SeeAlso `BackupAttachmentUploadProgress`
|
||||
///
|
||||
/// - SeeAlso ``BackupAttachmentDownloadTracker``
|
||||
@ -17,10 +17,14 @@ final class BackupAttachmentUploadTracker {
|
||||
struct UploadUpdate: Equatable {
|
||||
enum State {
|
||||
case running
|
||||
case suspended
|
||||
case empty
|
||||
case notRegisteredAndReady
|
||||
case pausedLowBattery
|
||||
case pausedLowPowerMode
|
||||
case pausedNeedsWifi
|
||||
case pausedNeedsInternet
|
||||
case hasConsumedMediaTierCapacity
|
||||
}
|
||||
|
||||
let state: State
|
||||
@ -47,21 +51,21 @@ final class BackupAttachmentUploadTracker {
|
||||
}
|
||||
}
|
||||
|
||||
private let backupAttachmentUploadQueueStatusReporter: BackupAttachmentUploadQueueStatusReporter
|
||||
private let backupAttachmentUploadQueueStatusManager: BackupAttachmentUploadQueueStatusManager
|
||||
private let backupAttachmentUploadProgress: BackupAttachmentUploadProgress
|
||||
|
||||
init(
|
||||
backupAttachmentUploadQueueStatusReporter: BackupAttachmentUploadQueueStatusReporter,
|
||||
backupAttachmentUploadQueueStatusManager: BackupAttachmentUploadQueueStatusManager,
|
||||
backupAttachmentUploadProgress: BackupAttachmentUploadProgress,
|
||||
) {
|
||||
self.backupAttachmentUploadQueueStatusReporter = backupAttachmentUploadQueueStatusReporter
|
||||
self.backupAttachmentUploadQueueStatusManager = backupAttachmentUploadQueueStatusManager
|
||||
self.backupAttachmentUploadProgress = backupAttachmentUploadProgress
|
||||
}
|
||||
|
||||
func updates() -> AsyncStream<UploadUpdate?> {
|
||||
func updates() -> AsyncStream<UploadUpdate> {
|
||||
return AsyncStream { continuation in
|
||||
let tracker = Tracker(
|
||||
backupAttachmentUploadQueueStatusReporter: backupAttachmentUploadQueueStatusReporter,
|
||||
backupAttachmentUploadQueueStatusManager: backupAttachmentUploadQueueStatusManager,
|
||||
backupAttachmentUploadProgress: backupAttachmentUploadProgress,
|
||||
continuation: continuation,
|
||||
)
|
||||
@ -95,19 +99,19 @@ private class Tracker {
|
||||
var uploadQueueStatusObserver: NotificationCenter.Observer?
|
||||
var uploadProgressObserver: BackupAttachmentUploadProgress.Observer?
|
||||
|
||||
let streamContinuation: AsyncStream<UploadUpdate?>.Continuation
|
||||
let streamContinuation: AsyncStream<UploadUpdate>.Continuation
|
||||
}
|
||||
|
||||
private let backupAttachmentUploadQueueStatusReporter: BackupAttachmentUploadQueueStatusReporter
|
||||
private let backupAttachmentUploadQueueStatusManager: BackupAttachmentUploadQueueStatusManager
|
||||
private let backupAttachmentUploadProgress: BackupAttachmentUploadProgress
|
||||
private let state: SeriallyAccessedState<State>
|
||||
|
||||
init(
|
||||
backupAttachmentUploadQueueStatusReporter: BackupAttachmentUploadQueueStatusReporter,
|
||||
backupAttachmentUploadQueueStatusManager: BackupAttachmentUploadQueueStatusManager,
|
||||
backupAttachmentUploadProgress: BackupAttachmentUploadProgress,
|
||||
continuation: AsyncStream<UploadUpdate?>.Continuation,
|
||||
continuation: AsyncStream<UploadUpdate>.Continuation,
|
||||
) {
|
||||
self.backupAttachmentUploadQueueStatusReporter = backupAttachmentUploadQueueStatusReporter
|
||||
self.backupAttachmentUploadQueueStatusManager = backupAttachmentUploadQueueStatusManager
|
||||
self.backupAttachmentUploadProgress = backupAttachmentUploadProgress
|
||||
self.state = SeriallyAccessedState(State(
|
||||
streamContinuation: continuation,
|
||||
@ -145,14 +149,14 @@ private class Tracker {
|
||||
guard let self else { return }
|
||||
|
||||
handleQueueStatusUpdate(
|
||||
backupAttachmentUploadQueueStatusReporter.currentStatus(for: .fullsize),
|
||||
backupAttachmentUploadQueueStatusManager.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),
|
||||
backupAttachmentUploadQueueStatusManager.beginObservingIfNecessary(for: .fullsize),
|
||||
)
|
||||
|
||||
return uploadQueueStatusObserver
|
||||
@ -213,38 +217,35 @@ private class Tracker {
|
||||
return
|
||||
}
|
||||
|
||||
guard lastReportedUploadProgress.totalUnitCount > 0 else {
|
||||
// We have no meaningful progress to report on.
|
||||
let uploadUpdateState: UploadUpdate.State
|
||||
switch lastReportedUploadQueueStatus {
|
||||
case .appBackgrounded:
|
||||
// Don't emit an update when the app is backgrounded, so callers are
|
||||
// left with the last update before backgrounding.
|
||||
return
|
||||
case .running:
|
||||
uploadUpdateState = .running
|
||||
case .suspended:
|
||||
uploadUpdateState = .suspended
|
||||
case .empty:
|
||||
uploadUpdateState = .empty
|
||||
case .notRegisteredAndReady:
|
||||
uploadUpdateState = .notRegisteredAndReady
|
||||
case .noReachability:
|
||||
uploadUpdateState = .pausedNeedsInternet
|
||||
case .noWifiReachability:
|
||||
uploadUpdateState = .pausedNeedsWifi
|
||||
case .lowBattery:
|
||||
uploadUpdateState = .pausedLowBattery
|
||||
case .lowPowerMode:
|
||||
uploadUpdateState = .pausedLowPowerMode
|
||||
case .hasConsumedMediaTierCapacity:
|
||||
uploadUpdateState = .hasConsumedMediaTierCapacity
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
streamContinuation.yield(UploadUpdate(
|
||||
state: uploadUpdateState,
|
||||
progress: lastReportedUploadProgress,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,9 +24,11 @@ final class BackupDisablingManager {
|
||||
private let backupAttachmentCoordinator: BackupAttachmentCoordinator
|
||||
private let backupAttachmentDownloadQueueStatusManager: BackupAttachmentDownloadQueueStatusManager
|
||||
private let backupCDNCredentialStore: BackupCDNCredentialStore
|
||||
private let backupExportJobStore: BackupExportJobStore
|
||||
private let backupKeyService: BackupKeyService
|
||||
private let backupPlanManager: BackupPlanManager
|
||||
private let backupSettingsStore: BackupSettingsStore
|
||||
private let clvBackupProgressViewStore: CLVBackupProgressView.Store
|
||||
private let db: DB
|
||||
private let kvStore: KeyValueStore
|
||||
private let logger: PrefixedLogger
|
||||
@ -39,9 +41,11 @@ final class BackupDisablingManager {
|
||||
backupAttachmentCoordinator: BackupAttachmentCoordinator,
|
||||
backupAttachmentDownloadQueueStatusManager: BackupAttachmentDownloadQueueStatusManager,
|
||||
backupCDNCredentialStore: BackupCDNCredentialStore,
|
||||
backupExportJobStore: BackupExportJobStore,
|
||||
backupKeyService: BackupKeyService,
|
||||
backupPlanManager: BackupPlanManager,
|
||||
backupSettingsStore: BackupSettingsStore,
|
||||
clvBackupProgressViewStore: CLVBackupProgressView.Store,
|
||||
db: DB,
|
||||
tsAccountManager: TSAccountManager,
|
||||
) {
|
||||
@ -50,9 +54,11 @@ final class BackupDisablingManager {
|
||||
self.backupAttachmentCoordinator = backupAttachmentCoordinator
|
||||
self.backupAttachmentDownloadQueueStatusManager = backupAttachmentDownloadQueueStatusManager
|
||||
self.backupCDNCredentialStore = backupCDNCredentialStore
|
||||
self.backupExportJobStore = backupExportJobStore
|
||||
self.backupKeyService = backupKeyService
|
||||
self.backupPlanManager = backupPlanManager
|
||||
self.backupSettingsStore = backupSettingsStore
|
||||
self.clvBackupProgressViewStore = clvBackupProgressViewStore
|
||||
self.db = db
|
||||
self.kvStore = KeyValueStore(collection: "BackupDisablingManager")
|
||||
self.logger = PrefixedLogger(prefix: "[Backups]")
|
||||
@ -212,6 +218,10 @@ final class BackupDisablingManager {
|
||||
// Wipe these, which are now outdated.
|
||||
backupSettingsStore.resetLastBackupDetails(tx: tx)
|
||||
backupSettingsStore.resetShouldAllowBackupUploadsOnCellular(tx: tx)
|
||||
backupExportJobStore.wipe(tx: tx)
|
||||
|
||||
// We want to re-show this if we reenable Backups later.
|
||||
clvBackupProgressViewStore.setIsHidden(false, tx: tx)
|
||||
|
||||
// With Backups disabled, these credentials are no longer valid
|
||||
// and are no longer safe to use.
|
||||
|
||||
@ -264,7 +264,7 @@ class BackupSettingsViewController:
|
||||
|
||||
let downloadViewUpdateState: BackupAttachmentDownloadProgressView.DownloadUpdate.State
|
||||
switch downloadTrackerUpdate.state {
|
||||
case .empty, .appBackgrounded, .notRegisteredAndReady:
|
||||
case .empty, .notRegisteredAndReady:
|
||||
viewModel.latestBackupAttachmentDownloadUpdate = nil
|
||||
return false
|
||||
case .running:
|
||||
@ -296,10 +296,36 @@ class BackupSettingsViewController:
|
||||
await deviceSleepManager.manageBlockForUpdateStream(
|
||||
backupAttachmentUploadTracker.updates(),
|
||||
label: "BackupSettings.BackupUploads",
|
||||
) { [weak self] uploadUpdate in
|
||||
) { [weak self] uploadTrackerUpdate in
|
||||
guard let self else { return false }
|
||||
viewModel.latestBackupAttachmentUploadUpdate = uploadUpdate
|
||||
return uploadUpdate != nil
|
||||
|
||||
let uploadViewUpdateState: BackupAttachmentUploadProgressView.UploadUpdate.State
|
||||
switch uploadTrackerUpdate.state {
|
||||
case .empty,
|
||||
.suspended,
|
||||
.notRegisteredAndReady,
|
||||
.hasConsumedMediaTierCapacity:
|
||||
viewModel.latestBackupAttachmentUploadUpdate = nil
|
||||
return false
|
||||
case .running:
|
||||
uploadViewUpdateState = .running
|
||||
case .pausedLowBattery:
|
||||
uploadViewUpdateState = .pausedLowBattery
|
||||
case .pausedLowPowerMode:
|
||||
uploadViewUpdateState = .pausedLowPowerMode
|
||||
case .pausedNeedsWifi:
|
||||
uploadViewUpdateState = .pausedNeedsWifi
|
||||
case .pausedNeedsInternet:
|
||||
uploadViewUpdateState = .pausedNeedsInternet
|
||||
}
|
||||
|
||||
viewModel.latestBackupAttachmentUploadUpdate = BackupAttachmentUploadProgressView.UploadUpdate(
|
||||
state: uploadViewUpdateState,
|
||||
bytesUploaded: uploadTrackerUpdate.bytesUploaded,
|
||||
totalBytesToUpload: uploadTrackerUpdate.totalBytesToUpload,
|
||||
percentageUploaded: uploadTrackerUpdate.percentageUploaded,
|
||||
)
|
||||
return true
|
||||
}
|
||||
},
|
||||
Task.detached { [weak self] in
|
||||
@ -1455,7 +1481,7 @@ private class BackupSettingsViewModel: ObservableObject {
|
||||
|
||||
@Published var latestBackupExportProgressUpdate: OWSSequentialProgress<BackupExportJobStage>?
|
||||
@Published var latestBackupAttachmentDownloadUpdate: BackupAttachmentDownloadProgressView.DownloadUpdate?
|
||||
@Published var latestBackupAttachmentUploadUpdate: BackupAttachmentUploadTracker.UploadUpdate?
|
||||
@Published var latestBackupAttachmentUploadUpdate: BackupAttachmentUploadProgressView.UploadUpdate?
|
||||
|
||||
@Published var lastBackupDetails: BackupSettingsStore.LastBackupDetails?
|
||||
@Published var shouldAllowBackupUploadsOnCellular: Bool
|
||||
@ -1482,7 +1508,7 @@ private class BackupSettingsViewModel: ObservableObject {
|
||||
failedToDisableBackupsRemotely: Bool,
|
||||
latestBackupExportProgressUpdate: OWSSequentialProgress<BackupExportJobStage>?,
|
||||
latestBackupAttachmentDownloadUpdate: BackupAttachmentDownloadProgressView.DownloadUpdate?,
|
||||
latestBackupAttachmentUploadUpdate: BackupAttachmentUploadTracker.UploadUpdate?,
|
||||
latestBackupAttachmentUploadUpdate: BackupAttachmentUploadProgressView.UploadUpdate?,
|
||||
lastBackupDetails: BackupSettingsStore.LastBackupDetails?,
|
||||
shouldAllowBackupUploadsOnCellular: Bool,
|
||||
mediaTierCapacityOverflow: UInt64?,
|
||||
@ -2077,7 +2103,7 @@ private struct BackupExportProgressView: View {
|
||||
}
|
||||
|
||||
let latestExportProgressUpdate: OWSSequentialProgress<BackupExportJobStage>
|
||||
let latestAttachmentUploadUpdate: BackupAttachmentUploadTracker.UploadUpdate?
|
||||
let latestAttachmentUploadUpdate: BackupAttachmentUploadProgressView.UploadUpdate?
|
||||
|
||||
private var progressBarState: ProgressBarState {
|
||||
switch latestExportProgressUpdate.currentStep {
|
||||
@ -2317,8 +2343,8 @@ private struct PulsingProgressBar: View {
|
||||
// MARK: -
|
||||
|
||||
private struct BackupAttachmentDownloadProgressView: View {
|
||||
struct DownloadUpdate {
|
||||
enum State {
|
||||
struct DownloadUpdate: Equatable {
|
||||
enum State: Equatable {
|
||||
case running
|
||||
case suspended
|
||||
case pausedLowBattery
|
||||
@ -2477,7 +2503,22 @@ private struct BackupAttachmentDownloadProgressView: View {
|
||||
// MARK: -
|
||||
|
||||
private struct BackupAttachmentUploadProgressView: View {
|
||||
let latestUploadUpdate: BackupAttachmentUploadTracker.UploadUpdate
|
||||
struct UploadUpdate: Equatable {
|
||||
enum State {
|
||||
case running
|
||||
case pausedLowBattery
|
||||
case pausedLowPowerMode
|
||||
case pausedNeedsWifi
|
||||
case pausedNeedsInternet
|
||||
}
|
||||
|
||||
let state: State
|
||||
let bytesUploaded: UInt64
|
||||
let totalBytesToUpload: UInt64
|
||||
let percentageUploaded: Float
|
||||
}
|
||||
|
||||
let latestUploadUpdate: UploadUpdate
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
@ -2495,7 +2536,7 @@ private struct BackupAttachmentUploadProgressView: View {
|
||||
}
|
||||
|
||||
static func subtitleText(
|
||||
uploadUpdate: BackupAttachmentUploadTracker.UploadUpdate?,
|
||||
uploadUpdate: BackupAttachmentUploadProgressView.UploadUpdate?,
|
||||
) -> String {
|
||||
guard let uploadUpdate else {
|
||||
return String(OWSLocalizedString(
|
||||
@ -3025,7 +3066,7 @@ private extension BackupSettingsViewModel {
|
||||
failedToDisableBackupsRemotely: Bool = false,
|
||||
latestBackupExportProgressUpdate: OWSSequentialProgress<BackupExportJobStage>? = nil,
|
||||
latestBackupAttachmentDownloadUpdateState: BackupAttachmentDownloadProgressView.DownloadUpdate.State? = nil,
|
||||
latestBackupAttachmentUploadUpdateState: BackupAttachmentUploadTracker.UploadUpdate.State? = nil,
|
||||
latestBackupAttachmentUploadUpdateState: BackupAttachmentUploadProgressView.UploadUpdate.State? = nil,
|
||||
mediaTierCapacityOverflow: UInt64? = nil,
|
||||
hasBackupFailed: Bool = false,
|
||||
isBackgroundAppRefreshDisabled: Bool = false,
|
||||
@ -3074,10 +3115,11 @@ private extension BackupSettingsViewModel {
|
||||
)
|
||||
},
|
||||
latestBackupAttachmentUploadUpdate: latestBackupAttachmentUploadUpdateState.map {
|
||||
BackupAttachmentUploadTracker.UploadUpdate(
|
||||
BackupAttachmentUploadProgressView.UploadUpdate(
|
||||
state: $0,
|
||||
bytesUploaded: 400_000_000,
|
||||
totalBytesToUpload: 1_600_000_000,
|
||||
percentageUploaded: 0.4 / 1.6,
|
||||
)
|
||||
},
|
||||
lastBackupDetails: BackupSettingsStore.LastBackupDetails(
|
||||
|
||||
6
Signal/Symbols.xcassets/eye/Contents.json
Normal file
6
Signal/Symbols.xcassets/eye/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
16
Signal/Symbols.xcassets/eye/eye-slash.imageset/Contents.json
vendored
Normal file
16
Signal/Symbols.xcassets/eye/eye-slash.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "eye-slash.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true,
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
Signal/Symbols.xcassets/eye/eye-slash.imageset/eye-slash.pdf
vendored
Normal file
BIN
Signal/Symbols.xcassets/eye/eye-slash.imageset/eye-slash.pdf
vendored
Normal file
Binary file not shown.
@ -408,47 +408,45 @@ class CLVTableDataSource: NSObject, UITableViewDataSource, UITableViewDelegate {
|
||||
contextMenuConfigurationForRowAt indexPath: IndexPath,
|
||||
point: CGPoint,
|
||||
) -> UIContextMenuConfiguration? {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let viewController = self.viewController else {
|
||||
owsFailDebug("Missing viewController.")
|
||||
return nil
|
||||
}
|
||||
guard viewController.canPresentPreview(fromIndexPath: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
guard let threadUniqueId = renderState.threadUniqueId(forIndexPath: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return UIContextMenuConfiguration(
|
||||
identifier: threadUniqueId as NSString,
|
||||
previewProvider: { [weak viewController] in
|
||||
viewController?.createPreviewController(atIndexPath: indexPath)
|
||||
},
|
||||
actionProvider: { _ in
|
||||
// nil for now. But we may want to add options like "Pin" or "Mute" in the future
|
||||
switch renderState.sections[indexPath.section].type {
|
||||
case .pinned,
|
||||
.unpinned:
|
||||
guard
|
||||
let chatListViewController = viewController,
|
||||
chatListViewController.canPresentPreview(fromIndexPath: indexPath),
|
||||
let threadUniqueId = renderState.threadUniqueId(forIndexPath: indexPath)
|
||||
else {
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return UIContextMenuConfiguration(
|
||||
identifier: threadUniqueId as NSString,
|
||||
previewProvider: {
|
||||
chatListViewController.createPreviewController(atIndexPath: indexPath)
|
||||
},
|
||||
)
|
||||
case .backupProgressView:
|
||||
let contextMenuActions = viewState.backupProgressView.contextMenuActions()
|
||||
|
||||
return UIContextMenuConfiguration(actionProvider: { _ in
|
||||
return UIMenu(children: contextMenuActions)
|
||||
})
|
||||
case .reminders,
|
||||
.backupDownloadProgressView,
|
||||
.archiveButton,
|
||||
.inboxFilterFooter:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(
|
||||
_ tableView: UITableView,
|
||||
previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration,
|
||||
) -> UITargetedPreview? {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let threadId = configuration.identifier as? String else {
|
||||
owsFailDebug("Unexpected context menu configuration identifier")
|
||||
return nil
|
||||
}
|
||||
guard let indexPath = renderState.indexPath(forUniqueId: threadId) else {
|
||||
Logger.warn("No index path for threadId: \(threadId).")
|
||||
return nil
|
||||
}
|
||||
guard tableView.window != nil else {
|
||||
Logger.warn("Dismissing tableView not in view hierarchy")
|
||||
guard
|
||||
let threadId = configuration.identifier as? String,
|
||||
let indexPath = renderState.indexPath(forUniqueId: threadId)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -45,7 +45,7 @@ public class CLVBackupDownloadProgressView {
|
||||
self.deviceSleepManager = deviceSleepManager
|
||||
|
||||
backupAttachmentDownloadProgressView = BackupAttachmentDownloadProgressView(
|
||||
backupAttachmentDownloadQueueStatusReporter: DependenciesBridge.shared.backupAttachmentDownloadQueueStatusManager,
|
||||
backupAttachmentDownloadQueueStatusManager: DependenciesBridge.shared.backupAttachmentDownloadQueueStatusManager,
|
||||
backupAttachmentDownloadStore: backupAttachmentDownloadStore,
|
||||
backupSettingsStore: backupSettingsStore,
|
||||
db: db,
|
||||
@ -191,7 +191,7 @@ public class CLVBackupDownloadProgressView {
|
||||
}
|
||||
|
||||
switch latestDownloadUpdate.state {
|
||||
case .suspended, .notRegisteredAndReady, .appBackgrounded:
|
||||
case .suspended, .notRegisteredAndReady:
|
||||
return nil
|
||||
case .empty:
|
||||
if
|
||||
@ -341,18 +341,18 @@ private class BackupAttachmentDownloadProgressView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
private let backupAttachmentDownloadQueueStatusReporter: BackupAttachmentDownloadQueueStatusReporter!
|
||||
private let backupAttachmentDownloadQueueStatusManager: BackupAttachmentDownloadQueueStatusManager!
|
||||
private let backupAttachmentDownloadStore: BackupAttachmentDownloadStore!
|
||||
private let backupSettingsStore: BackupSettingsStore!
|
||||
private let db: DB!
|
||||
|
||||
init(
|
||||
backupAttachmentDownloadQueueStatusReporter: BackupAttachmentDownloadQueueStatusReporter,
|
||||
backupAttachmentDownloadQueueStatusManager: BackupAttachmentDownloadQueueStatusManager,
|
||||
backupAttachmentDownloadStore: BackupAttachmentDownloadStore,
|
||||
backupSettingsStore: BackupSettingsStore,
|
||||
db: DB,
|
||||
) {
|
||||
self.backupAttachmentDownloadQueueStatusReporter = backupAttachmentDownloadQueueStatusReporter
|
||||
self.backupAttachmentDownloadQueueStatusManager = backupAttachmentDownloadQueueStatusManager
|
||||
self.backupAttachmentDownloadStore = backupAttachmentDownloadStore
|
||||
self.backupSettingsStore = backupSettingsStore
|
||||
self.db = db
|
||||
@ -369,7 +369,7 @@ private class BackupAttachmentDownloadProgressView: UIView {
|
||||
}
|
||||
|
||||
fileprivate init(forPreview: (), state: State) {
|
||||
self.backupAttachmentDownloadQueueStatusReporter = nil
|
||||
self.backupAttachmentDownloadQueueStatusManager = nil
|
||||
self.backupAttachmentDownloadStore = nil
|
||||
self.backupSettingsStore = nil
|
||||
self.db = nil
|
||||
@ -939,7 +939,7 @@ private class BackupAttachmentDownloadProgressView: UIView {
|
||||
action: { sheet in
|
||||
// Clear previous out of space errors, so they can try
|
||||
// again to download.
|
||||
self.backupAttachmentDownloadQueueStatusReporter.checkAvailableDiskSpace(
|
||||
self.backupAttachmentDownloadQueueStatusManager.checkAvailableDiskSpace(
|
||||
clearPreviousOutOfSpaceErrors: true,
|
||||
)
|
||||
sheet.dismiss(animated: true)
|
||||
|
||||
@ -5,24 +5,70 @@
|
||||
|
||||
import PureLayout
|
||||
import SignalServiceKit
|
||||
import SignalUI
|
||||
import UIKit
|
||||
|
||||
class CLVBackupProgressView {
|
||||
private extension Notification.Name {
|
||||
static let isHiddenDidChange = Notification.Name("CLVBackupProgressView.isHiddenDidChange")
|
||||
}
|
||||
|
||||
class CLVBackupProgressView: BackupProgressView.Delegate {
|
||||
|
||||
struct Store {
|
||||
private enum Keys {
|
||||
static let isHidden = "isHidden"
|
||||
static let earliestBackupDateToConsider = "earliestBackupDateToConsider"
|
||||
}
|
||||
|
||||
private let kvStore = NewKeyValueStore(collection: "CLVBackupProgressView")
|
||||
|
||||
func isHidden(tx: DBReadTransaction) -> Bool {
|
||||
return kvStore.fetchValue(Bool.self, forKey: Keys.isHidden, tx: tx) ?? false
|
||||
}
|
||||
|
||||
func setIsHidden(_ value: Bool, tx: DBWriteTransaction) {
|
||||
kvStore.writeValue(value, forKey: Keys.isHidden, tx: tx)
|
||||
|
||||
tx.addSyncCompletion {
|
||||
NotificationCenter.default.postOnMainThread(
|
||||
name: .isHiddenDidChange,
|
||||
object: nil,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func earliestBackupDateToConsider(tx: DBReadTransaction) -> Date? {
|
||||
kvStore.fetchValue(Date.self, forKey: Keys.earliestBackupDateToConsider, tx: tx)
|
||||
}
|
||||
|
||||
fileprivate func setEarliestBackupDateToConsider(_ value: Date, tx: DBWriteTransaction) {
|
||||
kvStore.writeValue(value, forKey: Keys.earliestBackupDateToConsider, tx: tx)
|
||||
}
|
||||
}
|
||||
|
||||
private struct State {
|
||||
var updateStreamTasks: [Task<Void, Never>] = []
|
||||
|
||||
var isVisible: Bool = false
|
||||
var deviceSleepBlock: DeviceSleepBlockObject?
|
||||
|
||||
var earliestBackupDateToConsider: Date = .distantFuture
|
||||
var isHidden: Bool = false
|
||||
var lastBackupDetails: BackupSettingsStore.LastBackupDetails?
|
||||
|
||||
// nil if we've never yet gotten an update. .some(nil) if we have gotten
|
||||
// an update, and that update was nil.
|
||||
var lastExportJobProgressUpdate: OWSSequentialProgress<BackupExportJobStage>??
|
||||
var lastUploadTrackerUpdate: BackupAttachmentUploadTracker.UploadUpdate?
|
||||
var lastExportJobUpdate: BackupExportJobRunnerUpdate?
|
||||
|
||||
var updateStreamTasks: [Task<Void, Never>] = []
|
||||
}
|
||||
|
||||
private let backupAttachmentUploadTracker: BackupAttachmentUploadTracker
|
||||
private let backupExportJobRunner: BackupExportJobRunner
|
||||
private let backupSettingsStore: BackupSettingsStore
|
||||
private let dateProvider: DateProvider
|
||||
private let db: DB
|
||||
private let deviceSleepManager: DeviceSleepManager
|
||||
private let store: Store
|
||||
|
||||
weak var chatListViewController: ChatListViewController?
|
||||
let backupProgressViewCell: UITableViewCell
|
||||
@ -33,8 +79,11 @@ class CLVBackupProgressView {
|
||||
init() {
|
||||
self.backupAttachmentUploadTracker = AppEnvironment.shared.backupAttachmentUploadTracker
|
||||
self.backupExportJobRunner = DependenciesBridge.shared.backupExportJobRunner
|
||||
self.backupSettingsStore = BackupSettingsStore()
|
||||
self.dateProvider = { Date() }
|
||||
self.db = DependenciesBridge.shared.db
|
||||
self.deviceSleepManager = DependenciesBridge.shared.deviceSleepManager.owsFailUnwrap("Missing DeviceSleepManager!")
|
||||
self.store = Store()
|
||||
|
||||
self.backupProgressViewCell = UITableViewCell()
|
||||
self.backupProgressViewCell.backgroundColor = .Signal.background
|
||||
@ -43,6 +92,7 @@ class CLVBackupProgressView {
|
||||
|
||||
self.backupProgressViewCell.contentView.addSubview(self.backupProgressView)
|
||||
self.backupProgressView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(hMargin: 12, vMargin: 12))
|
||||
self.backupProgressView.delegate = self
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
@ -76,8 +126,41 @@ class CLVBackupProgressView {
|
||||
// MARK: -
|
||||
|
||||
func startTracking() {
|
||||
let storedEarliestBackupDateToConsider: Date?
|
||||
let isHidden: Bool
|
||||
let lastBackupDetails: BackupSettingsStore.LastBackupDetails?
|
||||
(
|
||||
storedEarliestBackupDateToConsider,
|
||||
isHidden,
|
||||
lastBackupDetails,
|
||||
) = db.read { tx in
|
||||
(
|
||||
store.earliestBackupDateToConsider(tx: tx),
|
||||
store.isHidden(tx: tx),
|
||||
backupSettingsStore.lastBackupDetails(tx: tx),
|
||||
)
|
||||
}
|
||||
|
||||
// As a one-time migration, store "now" as the earliest Backup date to
|
||||
// consider. This avoids us showing the "Complete" state for users who
|
||||
// already had Backups enabled and running when we introduced this view.
|
||||
let earliestBackupDateToConsider: Date
|
||||
if let storedEarliestBackupDateToConsider {
|
||||
earliestBackupDateToConsider = storedEarliestBackupDateToConsider
|
||||
} else {
|
||||
earliestBackupDateToConsider = dateProvider()
|
||||
db.write { tx in
|
||||
store.setEarliestBackupDateToConsider(earliestBackupDateToConsider, tx: tx)
|
||||
}
|
||||
}
|
||||
|
||||
state.update { _state in
|
||||
guard _state.updateStreamTasks.isEmpty else { return }
|
||||
|
||||
_state.earliestBackupDateToConsider = earliestBackupDateToConsider
|
||||
_state.isHidden = isHidden
|
||||
_state.lastBackupDetails = lastBackupDetails
|
||||
|
||||
_state.updateStreamTasks = _startTracking()
|
||||
}
|
||||
}
|
||||
@ -89,7 +172,7 @@ class CLVBackupProgressView {
|
||||
guard let self else { return }
|
||||
|
||||
state.update { _state in
|
||||
_state.lastUploadTrackerUpdate = uploadTrackerUpdate
|
||||
_state.lastUploadTrackerUpdate = .some(uploadTrackerUpdate)
|
||||
self.setViewStateForState(state: &_state)
|
||||
}
|
||||
}
|
||||
@ -99,11 +182,46 @@ class CLVBackupProgressView {
|
||||
guard let self else { return }
|
||||
|
||||
state.update { _state in
|
||||
_state.lastExportJobUpdate = exportJobUpdate
|
||||
switch exportJobUpdate {
|
||||
case .progress(let progressUpdate):
|
||||
_state.lastExportJobProgressUpdate = progressUpdate
|
||||
case nil, .completion:
|
||||
_state.lastExportJobProgressUpdate = .some(nil)
|
||||
}
|
||||
self.setViewStateForState(state: &_state)
|
||||
}
|
||||
}
|
||||
},
|
||||
NotificationCenter.default.startTaskTrackingNotifications(
|
||||
named: .lastBackupDetailsDidChange,
|
||||
onNotification: { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
let lastBackupDetails = db.read { tx in
|
||||
self.backupSettingsStore.lastBackupDetails(tx: tx)
|
||||
}
|
||||
|
||||
state.update { _state in
|
||||
_state.lastBackupDetails = lastBackupDetails
|
||||
self.setViewStateForState(state: &_state)
|
||||
}
|
||||
},
|
||||
),
|
||||
NotificationCenter.default.startTaskTrackingNotifications(
|
||||
named: .isHiddenDidChange,
|
||||
onNotification: { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
let isHidden = db.read { tx in
|
||||
self.store.isHidden(tx: tx)
|
||||
}
|
||||
|
||||
state.update { _state in
|
||||
_state.isHidden = isHidden
|
||||
self.setViewStateForState(state: &_state)
|
||||
}
|
||||
},
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@ -135,37 +253,59 @@ class CLVBackupProgressView {
|
||||
}
|
||||
|
||||
private func viewStateForState(state: State) -> BackupProgressView.ViewState? {
|
||||
switch state.lastExportJobUpdate {
|
||||
case .progress(let sequentialProgress):
|
||||
switch sequentialProgress.currentStep {
|
||||
guard
|
||||
let lastExportJobProgressUpdate = state.lastExportJobProgressUpdate,
|
||||
let lastUploadTrackerUpdate = state.lastUploadTrackerUpdate
|
||||
else {
|
||||
// Never show the view until we've received our initial updates.
|
||||
return nil
|
||||
}
|
||||
|
||||
if state.isHidden {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let progressUpdate = lastExportJobProgressUpdate {
|
||||
switch progressUpdate.currentStep {
|
||||
case .backupFileExport, .backupFileUpload:
|
||||
let percentExportCompleted = sequentialProgress.progress(for: .backupFileExport)?.percentComplete ?? 0
|
||||
let percentUploadCompleted = sequentialProgress.progress(for: .backupFileUpload)?.percentComplete ?? 0
|
||||
let percentExportCompleted = progressUpdate.progress(for: .backupFileExport)?.percentComplete ?? 0
|
||||
let percentUploadCompleted = progressUpdate.progress(for: .backupFileUpload)?.percentComplete ?? 0
|
||||
let percentComplete = (0.95 * percentExportCompleted) + (0.05 * percentUploadCompleted)
|
||||
return .backupFilePreparation(percentComplete: percentComplete)
|
||||
case .attachmentUpload, .attachmentProcessing:
|
||||
break
|
||||
}
|
||||
case nil, .completion:
|
||||
break
|
||||
}
|
||||
|
||||
if let lastUploadTrackerUpdate = state.lastUploadTrackerUpdate {
|
||||
switch lastUploadTrackerUpdate.state {
|
||||
case .running:
|
||||
return .attachmentUploadRunning(
|
||||
bytesUploaded: lastUploadTrackerUpdate.bytesUploaded,
|
||||
totalBytesToUpload: lastUploadTrackerUpdate.totalBytesToUpload,
|
||||
)
|
||||
case .pausedLowBattery:
|
||||
return .attachmentUploadPausedLowBattery
|
||||
case .pausedLowPowerMode:
|
||||
return .attachmentUploadPausedLowPowerMode
|
||||
case .pausedNeedsWifi:
|
||||
return .attachmentUploadPausedNoWifi
|
||||
case .pausedNeedsInternet:
|
||||
return .attachmentUploadPausedNoInternet
|
||||
}
|
||||
switch lastUploadTrackerUpdate.state {
|
||||
case .empty:
|
||||
break
|
||||
case .running:
|
||||
return .attachmentUploadRunning(
|
||||
bytesUploaded: lastUploadTrackerUpdate.bytesUploaded,
|
||||
totalBytesToUpload: lastUploadTrackerUpdate.totalBytesToUpload,
|
||||
)
|
||||
case .suspended,
|
||||
.notRegisteredAndReady,
|
||||
.hasConsumedMediaTierCapacity:
|
||||
return nil
|
||||
case .pausedLowBattery:
|
||||
return .attachmentUploadPausedLowBattery
|
||||
case .pausedLowPowerMode:
|
||||
return .attachmentUploadPausedLowPowerMode
|
||||
case .pausedNeedsWifi:
|
||||
return .attachmentUploadPausedNoWifi
|
||||
case .pausedNeedsInternet:
|
||||
return .attachmentUploadPausedNoInternet
|
||||
}
|
||||
|
||||
// Check this after uploads, since we don't want to show "complete"
|
||||
// until uploads are done even if we've made a Backup file.
|
||||
if
|
||||
let lastBackupDetails = state.lastBackupDetails,
|
||||
lastBackupDetails.date > state.earliestBackupDateToConsider
|
||||
{
|
||||
return .complete
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -181,7 +321,6 @@ class CLVBackupProgressView {
|
||||
case .attachmentUploadPausedLowBattery: false
|
||||
case .attachmentUploadPausedLowPowerMode: false
|
||||
case .complete: false
|
||||
case .failed: false
|
||||
case nil: false
|
||||
}
|
||||
|
||||
@ -201,12 +340,102 @@ class CLVBackupProgressView {
|
||||
deviceSleepManager.removeBlock(blockObject: deviceSleepBlock)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ExportProgressView.Delegate
|
||||
|
||||
func didTapDismissButton() {
|
||||
db.write { tx in
|
||||
store.setIsHidden(true, tx: tx)
|
||||
}
|
||||
}
|
||||
|
||||
func didTapPausedWifiResumeButton() {
|
||||
let actionSheet = ActionSheetController(
|
||||
title: "Resume Using Cellular Data?",
|
||||
message: "Backing up your media using cellular data may result in data charges. Your backup may take a long time to upload, keep Signal open to avoid interruptions.",
|
||||
)
|
||||
actionSheet.addAction(ActionSheetAction(
|
||||
title: "Resume",
|
||||
handler: { [self] _ in
|
||||
db.write { tx in
|
||||
backupSettingsStore.setShouldAllowBackupUploadsOnCellular(true, tx: tx)
|
||||
}
|
||||
},
|
||||
))
|
||||
actionSheet.addAction(ActionSheetAction(
|
||||
title: "Later on Wi-Fi",
|
||||
))
|
||||
|
||||
chatListViewController?.presentActionSheet(actionSheet)
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Actions to display in a context menu for the owning row in the table
|
||||
/// view.
|
||||
func contextMenuActions() -> [UIAction] {
|
||||
let hideAction = UIAction(title: "Hide", image: .eyeSlash) { [self] _ in
|
||||
db.write { tx in
|
||||
store.setIsHidden(true, tx: tx)
|
||||
}
|
||||
chatListViewController?.presentToast(text: "View backup progress in Backup Settings")
|
||||
}
|
||||
|
||||
let cancelAction = UIAction(title: "Cancel backup", image: .xCircle) { [self] _ in
|
||||
let actionSheet = ActionSheetController(
|
||||
title: "Cancel Backup?",
|
||||
message: "Canceling your backup will not delete your backup. You can resume your backup at any time from Backup Settings.",
|
||||
)
|
||||
actionSheet.addAction(ActionSheetAction(
|
||||
title: "Cancel Backup",
|
||||
handler: { [self] _ in
|
||||
// Cancel the BackupExportJob, and pause uploads.
|
||||
backupExportJobRunner.cancelIfRunning()
|
||||
db.write {
|
||||
backupSettingsStore.setIsBackupUploadQueueSuspended(true, tx: $0)
|
||||
}
|
||||
chatListViewController?.presentToast(text: "Backup canceled")
|
||||
},
|
||||
))
|
||||
actionSheet.addAction(ActionSheetAction(
|
||||
title: "Continue Backup",
|
||||
))
|
||||
chatListViewController?.presentActionSheet(actionSheet)
|
||||
}
|
||||
|
||||
switch backupProgressView.viewState {
|
||||
case nil:
|
||||
return []
|
||||
case .complete:
|
||||
return [hideAction]
|
||||
case .backupFilePreparation,
|
||||
.attachmentUploadRunning,
|
||||
.attachmentUploadPausedNoWifi,
|
||||
.attachmentUploadPausedNoInternet,
|
||||
.attachmentUploadPausedLowBattery,
|
||||
.attachmentUploadPausedLowPowerMode:
|
||||
return [hideAction, cancelAction]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension ChatListViewController {
|
||||
func handleBackupProgressViewTapped() {
|
||||
SignalApp.shared.showAppSettings(mode: .backups())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private class BackupProgressView: UIView {
|
||||
|
||||
protocol Delegate: AnyObject {
|
||||
func didTapDismissButton()
|
||||
func didTapPausedWifiResumeButton()
|
||||
}
|
||||
|
||||
enum ViewState: Equatable, Identifiable {
|
||||
case backupFilePreparation(percentComplete: Float)
|
||||
case attachmentUploadRunning(bytesUploaded: UInt64, totalBytesToUpload: UInt64)
|
||||
@ -215,7 +444,6 @@ private class BackupProgressView: UIView {
|
||||
case attachmentUploadPausedLowBattery
|
||||
case attachmentUploadPausedLowPowerMode
|
||||
case complete
|
||||
case failed
|
||||
|
||||
var id: String {
|
||||
return switch self {
|
||||
@ -226,7 +454,6 @@ private class BackupProgressView: UIView {
|
||||
case .attachmentUploadPausedLowBattery: "attachmentUploadPausedLowBattery"
|
||||
case .attachmentUploadPausedLowPowerMode: "attachmentUploadPausedLowPowerMode"
|
||||
case .complete: "complete"
|
||||
case .failed: "failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -265,7 +492,8 @@ private class BackupProgressView: UIView {
|
||||
private let trailingAccessoryPausedLowBatteryLabel = UILabel()
|
||||
private let trailingAccessoryPausedLowPowerModeLabel = UILabel()
|
||||
private let trailingAccessoryCompleteDismissButton = UIButton()
|
||||
private let trailingAccessoryFailedDetailsButton = UIButton()
|
||||
|
||||
weak var delegate: Delegate?
|
||||
|
||||
init(viewState: ViewState?) {
|
||||
self.viewState = viewState
|
||||
@ -311,7 +539,7 @@ private class BackupProgressView: UIView {
|
||||
trailingAccessoryPausedWifiResumeButton.titleLabel?.adjustsFontForContentSizeCategory = true
|
||||
trailingAccessoryPausedWifiResumeButton.addAction(
|
||||
UIAction { [weak self] _ in
|
||||
self?.didTapPausedWifiResumeButton()
|
||||
self?.delegate?.didTapPausedWifiResumeButton()
|
||||
},
|
||||
for: .touchUpInside,
|
||||
)
|
||||
@ -322,7 +550,7 @@ private class BackupProgressView: UIView {
|
||||
trailingAccessoryCompleteDismissButton.tintColor = .Signal.secondaryLabel
|
||||
trailingAccessoryCompleteDismissButton.addAction(
|
||||
UIAction { [weak self] _ in
|
||||
self?.didTapDismissButton()
|
||||
self?.delegate?.didTapDismissButton()
|
||||
},
|
||||
for: .touchUpInside,
|
||||
)
|
||||
@ -339,22 +567,6 @@ private class BackupProgressView: UIView {
|
||||
Self.configure(label: trailingAccessoryPausedLowPowerModeLabel, color: .Signal.secondaryLabel)
|
||||
trailingAccessoryPausedLowPowerModeLabel.text = "Low Power Mode…"
|
||||
|
||||
trailingAccessoryContainerView.addArrangedSubview(trailingAccessoryFailedDetailsButton)
|
||||
trailingAccessoryFailedDetailsButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
trailingAccessoryFailedDetailsButton.setTitle(
|
||||
"See Details",
|
||||
for: .normal,
|
||||
)
|
||||
trailingAccessoryFailedDetailsButton.setTitleColor(.Signal.label, for: .normal)
|
||||
trailingAccessoryFailedDetailsButton.titleLabel?.font = .dynamicTypeSubheadline.semibold()
|
||||
trailingAccessoryFailedDetailsButton.titleLabel?.adjustsFontForContentSizeCategory = true
|
||||
trailingAccessoryFailedDetailsButton.addAction(
|
||||
UIAction { [weak self] _ in
|
||||
self?.didTapFailedDetailsButton()
|
||||
},
|
||||
for: .touchUpInside,
|
||||
)
|
||||
|
||||
initializeConstraints()
|
||||
configureSubviewsForCurrentState()
|
||||
}
|
||||
@ -380,9 +592,6 @@ private class BackupProgressView: UIView {
|
||||
case .complete:
|
||||
leadingAccessoryImageView.image = .checkCircle
|
||||
leadingAccessoryImageView.tintColor = .Signal.ultramarine
|
||||
case .failed:
|
||||
leadingAccessoryImageView.image = .errorCircle
|
||||
leadingAccessoryImageView.tintColor = .Signal.orange
|
||||
}
|
||||
|
||||
// Labels
|
||||
@ -409,8 +618,6 @@ private class BackupProgressView: UIView {
|
||||
titleLabelText = "Backup paused"
|
||||
case .complete:
|
||||
titleLabelText = "Backup complete"
|
||||
case .failed:
|
||||
titleLabelText = "Backup failed"
|
||||
case nil:
|
||||
titleLabelText = ""
|
||||
}
|
||||
@ -441,8 +648,6 @@ private class BackupProgressView: UIView {
|
||||
trailingAccessoryView = trailingAccessoryPausedLowPowerModeLabel
|
||||
case .complete:
|
||||
trailingAccessoryView = trailingAccessoryCompleteDismissButton
|
||||
case .failed:
|
||||
trailingAccessoryView = trailingAccessoryFailedDetailsButton
|
||||
case nil:
|
||||
trailingAccessoryView = nil
|
||||
}
|
||||
@ -481,32 +686,6 @@ private class BackupProgressView: UIView {
|
||||
trailingAccessoryCompleteDismissButton.widthAnchor.constraint(equalToConstant: 24),
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private func didTapDismissButton() {
|
||||
// TODO: Implement
|
||||
print(#function)
|
||||
}
|
||||
|
||||
private func didTapPausedWifiResumeButton() {
|
||||
// TODO: Implement
|
||||
print(#function)
|
||||
}
|
||||
|
||||
private func didTapFailedDetailsButton() {
|
||||
// TODO: Implement
|
||||
print(#function)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension ChatListViewController {
|
||||
func handleBackupProgressViewTapped() {
|
||||
// TODO: Implement
|
||||
print(#function)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
@ -573,11 +752,6 @@ private class BackupProgressViewPreviewViewController: UIViewController {
|
||||
return BackupProgressViewPreviewViewController(state: .complete)
|
||||
}
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview("Failed") {
|
||||
return BackupProgressViewPreviewViewController(state: .failed)
|
||||
}
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview("Nil") {
|
||||
return BackupProgressViewPreviewViewController(state: nil)
|
||||
|
||||
@ -19,10 +19,10 @@ final class BackupAttachmentDownloadTrackerTest: BackupAttachmentTrackerTest<
|
||||
@Test
|
||||
func testLaunchingWithQueuePopulated() async {
|
||||
let downloadProgress = MockAttachmentDownloadProgress(total: 4)
|
||||
let downloadQueueStatusReporter = MockDownloadQueueStatusReporter(.running)
|
||||
let downloadQueueStatusManager = MockDownloadQueueStatusManager(.running)
|
||||
|
||||
let downloadTracker = BackupAttachmentDownloadTracker(
|
||||
backupAttachmentDownloadQueueStatusReporter: downloadQueueStatusReporter,
|
||||
backupAttachmentDownloadQueueStatusManager: downloadQueueStatusManager,
|
||||
backupAttachmentDownloadProgress: downloadProgress,
|
||||
)
|
||||
|
||||
@ -42,7 +42,7 @@ final class BackupAttachmentDownloadTrackerTest: BackupAttachmentTrackerTest<
|
||||
ExpectedUpdate(
|
||||
update: DownloadUpdate(.running, downloaded: 4, total: 4),
|
||||
nextSteps: {
|
||||
downloadQueueStatusReporter.currentStatusMock = .empty
|
||||
downloadQueueStatusManager.currentStatusMock = .empty
|
||||
},
|
||||
),
|
||||
ExpectedUpdate(
|
||||
@ -59,10 +59,10 @@ final class BackupAttachmentDownloadTrackerTest: BackupAttachmentTrackerTest<
|
||||
@Test
|
||||
func testQueueStartsSuspendedThenStartsRunning() async {
|
||||
let downloadProgress = MockAttachmentDownloadProgress(total: 4)
|
||||
let downloadQueueStatusReporter = MockDownloadQueueStatusReporter(.suspended)
|
||||
let downloadQueueStatusManager = MockDownloadQueueStatusManager(.suspended)
|
||||
|
||||
let downloadTracker = BackupAttachmentDownloadTracker(
|
||||
backupAttachmentDownloadQueueStatusReporter: downloadQueueStatusReporter,
|
||||
backupAttachmentDownloadQueueStatusManager: downloadQueueStatusManager,
|
||||
backupAttachmentDownloadProgress: downloadProgress,
|
||||
)
|
||||
|
||||
@ -70,7 +70,7 @@ final class BackupAttachmentDownloadTrackerTest: BackupAttachmentTrackerTest<
|
||||
ExpectedUpdate(
|
||||
update: DownloadUpdate(.suspended, downloaded: 0, total: 4),
|
||||
nextSteps: {
|
||||
downloadQueueStatusReporter.currentStatusMock = .running
|
||||
downloadQueueStatusManager.currentStatusMock = .running
|
||||
},
|
||||
),
|
||||
ExpectedUpdate(
|
||||
@ -88,7 +88,7 @@ final class BackupAttachmentDownloadTrackerTest: BackupAttachmentTrackerTest<
|
||||
ExpectedUpdate(
|
||||
update: DownloadUpdate(.running, downloaded: 4, total: 4),
|
||||
nextSteps: {
|
||||
downloadQueueStatusReporter.currentStatusMock = .empty
|
||||
downloadQueueStatusManager.currentStatusMock = .empty
|
||||
},
|
||||
),
|
||||
ExpectedUpdate(
|
||||
@ -107,10 +107,10 @@ final class BackupAttachmentDownloadTrackerTest: BackupAttachmentTrackerTest<
|
||||
@Test
|
||||
func testQueueRunsIntoLowStorage_remainingMoreThanMin() async {
|
||||
let downloadProgress = MockAttachmentDownloadProgress(precompleted: 50, total: 100)
|
||||
let downloadQueueStatusReporter = MockDownloadQueueStatusReporter(.running, minimumRequiredDiskSpace: 10)
|
||||
let downloadQueueStatusManager = MockDownloadQueueStatusManager(.running, minimumRequiredDiskSpace: 10)
|
||||
|
||||
let downloadTracker = BackupAttachmentDownloadTracker(
|
||||
backupAttachmentDownloadQueueStatusReporter: downloadQueueStatusReporter,
|
||||
backupAttachmentDownloadQueueStatusManager: downloadQueueStatusManager,
|
||||
backupAttachmentDownloadProgress: downloadProgress,
|
||||
)
|
||||
|
||||
@ -118,7 +118,7 @@ final class BackupAttachmentDownloadTrackerTest: BackupAttachmentTrackerTest<
|
||||
ExpectedUpdate(
|
||||
update: DownloadUpdate(.running, downloaded: 50, total: 100),
|
||||
nextSteps: {
|
||||
downloadQueueStatusReporter.currentStatusMock = .lowDiskSpace
|
||||
downloadQueueStatusManager.currentStatusMock = .lowDiskSpace
|
||||
},
|
||||
),
|
||||
ExpectedUpdate(
|
||||
@ -137,10 +137,10 @@ final class BackupAttachmentDownloadTrackerTest: BackupAttachmentTrackerTest<
|
||||
@Test
|
||||
func testQueueRunsIntoLowStorage_remainingLessThanMin() async {
|
||||
let downloadProgress = MockAttachmentDownloadProgress(precompleted: 4, total: 12)
|
||||
let downloadQueueStatusReporter = MockDownloadQueueStatusReporter(.running, minimumRequiredDiskSpace: 10)
|
||||
let downloadQueueStatusManager = MockDownloadQueueStatusManager(.running, minimumRequiredDiskSpace: 10)
|
||||
|
||||
let downloadTracker = BackupAttachmentDownloadTracker(
|
||||
backupAttachmentDownloadQueueStatusReporter: downloadQueueStatusReporter,
|
||||
backupAttachmentDownloadQueueStatusManager: downloadQueueStatusManager,
|
||||
backupAttachmentDownloadProgress: downloadProgress,
|
||||
)
|
||||
|
||||
@ -148,7 +148,7 @@ final class BackupAttachmentDownloadTrackerTest: BackupAttachmentTrackerTest<
|
||||
ExpectedUpdate(
|
||||
update: DownloadUpdate(.running, downloaded: 4, total: 12),
|
||||
nextSteps: {
|
||||
downloadQueueStatusReporter.currentStatusMock = .lowDiskSpace
|
||||
downloadQueueStatusManager.currentStatusMock = .lowDiskSpace
|
||||
},
|
||||
),
|
||||
ExpectedUpdate(
|
||||
@ -165,10 +165,10 @@ final class BackupAttachmentDownloadTrackerTest: BackupAttachmentTrackerTest<
|
||||
@Test
|
||||
func testTrackingStoppingAndReTracking() async {
|
||||
let downloadProgress = MockAttachmentDownloadProgress(total: 4)
|
||||
let downloadQueueStatusReporter = MockDownloadQueueStatusReporter(.empty)
|
||||
let downloadQueueStatusManager = MockDownloadQueueStatusManager(.empty)
|
||||
|
||||
let downloadTracker = BackupAttachmentDownloadTracker(
|
||||
backupAttachmentDownloadQueueStatusReporter: downloadQueueStatusReporter,
|
||||
backupAttachmentDownloadQueueStatusManager: downloadQueueStatusManager,
|
||||
backupAttachmentDownloadProgress: downloadProgress,
|
||||
)
|
||||
|
||||
@ -176,7 +176,7 @@ final class BackupAttachmentDownloadTrackerTest: BackupAttachmentTrackerTest<
|
||||
ExpectedUpdate(
|
||||
update: DownloadUpdate(.empty, downloaded: 0, total: 4),
|
||||
nextSteps: {
|
||||
downloadQueueStatusReporter.currentStatusMock = .running
|
||||
downloadQueueStatusManager.currentStatusMock = .running
|
||||
},
|
||||
),
|
||||
ExpectedUpdate(
|
||||
@ -196,7 +196,7 @@ final class BackupAttachmentDownloadTrackerTest: BackupAttachmentTrackerTest<
|
||||
ExpectedUpdate(
|
||||
update: DownloadUpdate(.running, downloaded: 1, total: 1),
|
||||
nextSteps: {
|
||||
downloadQueueStatusReporter.currentStatusMock = .empty
|
||||
downloadQueueStatusManager.currentStatusMock = .empty
|
||||
},
|
||||
),
|
||||
ExpectedUpdate(
|
||||
@ -210,10 +210,10 @@ final class BackupAttachmentDownloadTrackerTest: BackupAttachmentTrackerTest<
|
||||
@Test
|
||||
func testTrackingMultipleStreamInstances() async {
|
||||
let downloadProgress = MockAttachmentDownloadProgress(total: 1)
|
||||
let downloadQueueStatusReporter = MockDownloadQueueStatusReporter(.empty)
|
||||
let downloadQueueStatusManager = MockDownloadQueueStatusManager(.empty)
|
||||
|
||||
let downloadTracker = BackupAttachmentDownloadTracker(
|
||||
backupAttachmentDownloadQueueStatusReporter: downloadQueueStatusReporter,
|
||||
backupAttachmentDownloadQueueStatusManager: downloadQueueStatusManager,
|
||||
backupAttachmentDownloadProgress: downloadProgress,
|
||||
)
|
||||
|
||||
@ -221,7 +221,7 @@ final class BackupAttachmentDownloadTrackerTest: BackupAttachmentTrackerTest<
|
||||
ExpectedUpdate(
|
||||
update: DownloadUpdate(.empty, downloaded: 0, total: 1),
|
||||
nextSteps: {
|
||||
downloadQueueStatusReporter.currentStatusMock = .running
|
||||
downloadQueueStatusManager.currentStatusMock = .running
|
||||
},
|
||||
),
|
||||
ExpectedUpdate(
|
||||
@ -233,7 +233,7 @@ final class BackupAttachmentDownloadTrackerTest: BackupAttachmentTrackerTest<
|
||||
ExpectedUpdate(
|
||||
update: DownloadUpdate(.running, downloaded: 1, total: 1),
|
||||
nextSteps: {
|
||||
downloadQueueStatusReporter.currentStatusMock = .empty
|
||||
downloadQueueStatusManager.currentStatusMock = .empty
|
||||
},
|
||||
),
|
||||
ExpectedUpdate(
|
||||
@ -282,7 +282,7 @@ private class MockAttachmentDownloadProgress: BackupAttachmentDownloadProgressMo
|
||||
|
||||
// MARK: -
|
||||
|
||||
private class MockDownloadQueueStatusReporter: BackupAttachmentDownloadQueueStatusReporter {
|
||||
private class MockDownloadQueueStatusManager: BackupAttachmentDownloadQueueStatusManager {
|
||||
init(
|
||||
_ initialStatus: BackupAttachmentDownloadQueueStatus,
|
||||
minimumRequiredDiskSpace: UInt64 = 0,
|
||||
@ -310,7 +310,7 @@ private class MockDownloadQueueStatusReporter: BackupAttachmentDownloadQueueStat
|
||||
return currentStatusMock
|
||||
}
|
||||
|
||||
func currentStatusAndToken(for mode: BackupAttachmentDownloadQueueMode) -> (SignalServiceKit.BackupAttachmentDownloadQueueStatus, any SignalServiceKit.BackupAttachmentDownloadQueueStatusToken) {
|
||||
func currentStatusAndToken(for mode: BackupAttachmentDownloadQueueMode) -> (BackupAttachmentDownloadQueueStatus, BackupAttachmentDownloadQueueStatusToken) {
|
||||
switch mode {
|
||||
case .fullsize:
|
||||
break
|
||||
@ -320,12 +320,32 @@ private class MockDownloadQueueStatusReporter: BackupAttachmentDownloadQueueStat
|
||||
return (currentStatusMock, MockBackupAttachmentDownloadQueueStatusManager.BackupAttachmentDownloadQueueStatusTokenMock())
|
||||
}
|
||||
|
||||
func beginObservingIfNecessary(for mode: BackupAttachmentDownloadQueueMode) -> BackupAttachmentDownloadQueueStatus {
|
||||
return currentStatusMock
|
||||
}
|
||||
|
||||
func jobDidExperienceError(_ error: any Error, token: any BackupAttachmentDownloadQueueStatusToken, mode: BackupAttachmentDownloadQueueMode) async -> BackupAttachmentDownloadQueueStatus? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func jobDidSucceed(token: any BackupAttachmentDownloadQueueStatusToken, mode: BackupAttachmentDownloadQueueMode) async {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
func didEmptyQueue(for mode: BackupAttachmentDownloadQueueMode) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
func setIsMainAppAndActiveOverride(_ newValue: Bool) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
nonisolated let minimumRequiredDiskSpaceMock: UInt64
|
||||
func minimumRequiredDiskSpaceToCompleteDownloads() -> UInt64 {
|
||||
minimumRequiredDiskSpaceMock
|
||||
}
|
||||
|
||||
func checkAvailableDiskSpace(clearPreviousOutOfSpaceErrors: Bool) {
|
||||
// Nothing
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ import Testing
|
||||
@MainActor
|
||||
@Suite(.serialized)
|
||||
final class BackupAttachmentUploadTrackerTest: BackupAttachmentTrackerTest<
|
||||
BackupAttachmentUploadTracker.UploadUpdate?,
|
||||
BackupAttachmentUploadTracker.UploadUpdate,
|
||||
> {
|
||||
typealias UploadUpdate = BackupAttachmentUploadTracker.UploadUpdate
|
||||
|
||||
@ -19,9 +19,9 @@ final class BackupAttachmentUploadTrackerTest: BackupAttachmentTrackerTest<
|
||||
@Test
|
||||
func testLaunchingWithQueuePopulated() async {
|
||||
let uploadProgress = MockAttachmentUploadProgress(total: 4)
|
||||
let uploadQueueStatusReporter = MockUploadQueueStatusReporter(.running)
|
||||
let uploadQueueStatusManager = MockUploadQueueStatusManager(.running)
|
||||
let uploadTracker = BackupAttachmentUploadTracker(
|
||||
backupAttachmentUploadQueueStatusReporter: uploadQueueStatusReporter,
|
||||
backupAttachmentUploadQueueStatusManager: uploadQueueStatusManager,
|
||||
backupAttachmentUploadProgress: uploadProgress,
|
||||
)
|
||||
|
||||
@ -41,10 +41,13 @@ final class BackupAttachmentUploadTrackerTest: BackupAttachmentTrackerTest<
|
||||
ExpectedUpdate(
|
||||
update: UploadUpdate(.running, uploaded: 4, total: 4),
|
||||
nextSteps: {
|
||||
uploadQueueStatusReporter.currentStatusMock = .empty
|
||||
uploadQueueStatusManager.currentStatusMock = .empty
|
||||
},
|
||||
),
|
||||
ExpectedUpdate(update: nil, nextSteps: {}),
|
||||
ExpectedUpdate(
|
||||
update: UploadUpdate(.empty, uploaded: 4, total: 4),
|
||||
nextSteps: {},
|
||||
),
|
||||
]
|
||||
|
||||
await runTest(updateStream: uploadTracker.updates(), expectedUpdates: expectedUpdates)
|
||||
@ -55,9 +58,9 @@ final class BackupAttachmentUploadTrackerTest: BackupAttachmentTrackerTest<
|
||||
@Test
|
||||
func testTrackingStoppingAndReTracking() async {
|
||||
let uploadProgress = MockAttachmentUploadProgress(total: 4)
|
||||
let uploadQueueStatusReporter = MockUploadQueueStatusReporter(.running)
|
||||
let uploadQueueStatusManager = MockUploadQueueStatusManager(.running)
|
||||
let uploadTracker = BackupAttachmentUploadTracker(
|
||||
backupAttachmentUploadQueueStatusReporter: uploadQueueStatusReporter,
|
||||
backupAttachmentUploadQueueStatusManager: uploadQueueStatusManager,
|
||||
backupAttachmentUploadProgress: uploadProgress,
|
||||
)
|
||||
|
||||
@ -79,11 +82,11 @@ final class BackupAttachmentUploadTrackerTest: BackupAttachmentTrackerTest<
|
||||
ExpectedUpdate(
|
||||
update: UploadUpdate(.running, uploaded: 1, total: 1),
|
||||
nextSteps: {
|
||||
uploadQueueStatusReporter.currentStatusMock = .empty
|
||||
uploadQueueStatusManager.currentStatusMock = .empty
|
||||
},
|
||||
),
|
||||
ExpectedUpdate(
|
||||
update: nil,
|
||||
update: UploadUpdate(.empty, uploaded: 1, total: 1),
|
||||
nextSteps: {},
|
||||
),
|
||||
]
|
||||
@ -93,9 +96,9 @@ final class BackupAttachmentUploadTrackerTest: BackupAttachmentTrackerTest<
|
||||
@Test
|
||||
func testTrackingMultipleStreamInstances() async {
|
||||
let uploadProgress = MockAttachmentUploadProgress(total: 1)
|
||||
let uploadQueueStatusReporter = MockUploadQueueStatusReporter(.running)
|
||||
let uploadQueueStatusManager = MockUploadQueueStatusManager(.running)
|
||||
let uploadTracker = BackupAttachmentUploadTracker(
|
||||
backupAttachmentUploadQueueStatusReporter: uploadQueueStatusReporter,
|
||||
backupAttachmentUploadQueueStatusManager: uploadQueueStatusManager,
|
||||
backupAttachmentUploadProgress: uploadProgress,
|
||||
)
|
||||
|
||||
@ -109,11 +112,11 @@ final class BackupAttachmentUploadTrackerTest: BackupAttachmentTrackerTest<
|
||||
ExpectedUpdate(
|
||||
update: UploadUpdate(.running, uploaded: 1, total: 1),
|
||||
nextSteps: {
|
||||
uploadQueueStatusReporter.currentStatusMock = .empty
|
||||
uploadQueueStatusManager.currentStatusMock = .empty
|
||||
},
|
||||
),
|
||||
ExpectedUpdate(
|
||||
update: nil,
|
||||
update: UploadUpdate(.empty, uploaded: 1, total: 1),
|
||||
nextSteps: {},
|
||||
),
|
||||
]
|
||||
@ -157,7 +160,7 @@ private class MockAttachmentUploadProgress: BackupAttachmentUploadProgressMock {
|
||||
|
||||
// MARK: -
|
||||
|
||||
private class MockUploadQueueStatusReporter: BackupAttachmentUploadQueueStatusReporter {
|
||||
private class MockUploadQueueStatusManager: BackupAttachmentUploadQueueStatusManager {
|
||||
var currentStatusMock: BackupAttachmentUploadQueueStatus {
|
||||
didSet {
|
||||
NotificationCenter.default.postOnMainThread(
|
||||
@ -172,12 +175,18 @@ private class MockUploadQueueStatusReporter: BackupAttachmentUploadQueueStatusRe
|
||||
}
|
||||
|
||||
func currentStatus(for mode: BackupAttachmentUploadQueueMode) -> BackupAttachmentUploadQueueStatus {
|
||||
switch mode {
|
||||
case .fullsize:
|
||||
break
|
||||
case .thumbnail:
|
||||
fatalError("Only use fullsize in these tests")
|
||||
}
|
||||
return currentStatusMock
|
||||
}
|
||||
|
||||
func beginObservingIfNecessary(for mode: BackupAttachmentUploadQueueMode) -> BackupAttachmentUploadQueueStatus {
|
||||
return currentStatusMock
|
||||
}
|
||||
|
||||
func didEmptyQueue(for mode: BackupAttachmentUploadQueueMode) {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
func setIsMainAppAndActiveOverride(_ newValue: Bool) {
|
||||
// Nothing
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,35 +53,6 @@ public extension Notification.Name {
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Reports whether we are able to download Backup attachments, via various
|
||||
/// consolidated inputs.
|
||||
///
|
||||
/// `@MainActor`-isolated because most of the inputs are themselves isolated.
|
||||
///
|
||||
/// - SeeAlso `BackupAttachmentDownloadTracker`
|
||||
@MainActor
|
||||
public protocol BackupAttachmentDownloadQueueStatusReporter {
|
||||
func currentStatus(for mode: BackupAttachmentDownloadQueueMode) -> BackupAttachmentDownloadQueueStatus
|
||||
|
||||
func currentStatusAndToken(for mode: BackupAttachmentDownloadQueueMode) -> (BackupAttachmentDownloadQueueStatus, BackupAttachmentDownloadQueueStatusToken)
|
||||
|
||||
/// Synchronously returns the minimum required disk space for downloads.
|
||||
nonisolated func minimumRequiredDiskSpaceToCompleteDownloads() -> UInt64
|
||||
|
||||
/// Check available disk space, optionally clearing in-memory state
|
||||
/// regarding past "out of space" errors.
|
||||
func checkAvailableDiskSpace(clearPreviousOutOfSpaceErrors: Bool)
|
||||
}
|
||||
|
||||
extension BackupAttachmentDownloadQueueStatusReporter {
|
||||
fileprivate func notifyStatusDidChange(for mode: BackupAttachmentDownloadQueueMode) {
|
||||
NotificationCenter.default.postOnMainThread(
|
||||
name: .backupAttachmentDownloadQueueStatusDidChange(mode: mode),
|
||||
object: nil,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Grab one of these when starting a job; use it to mark success or failure
|
||||
/// This takes a (black box) snapshot of state when the download began so that
|
||||
/// when we respond to success or errors we apply them appropriately based
|
||||
@ -90,10 +61,23 @@ public protocol BackupAttachmentDownloadQueueStatusToken {}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// API for callers to manage the `StatusReporter` in response to relevant
|
||||
/// external events.
|
||||
/// Tracks and reports the status of the Backup attachment download queue.
|
||||
///
|
||||
/// `@MainActor`-isolated because most of the inputs are themselves isolated.
|
||||
///
|
||||
/// - SeeAlso `BackupAttachmentDownloadTracker`
|
||||
@MainActor
|
||||
public protocol BackupAttachmentDownloadQueueStatusManager: BackupAttachmentDownloadQueueStatusReporter {
|
||||
public protocol BackupAttachmentDownloadQueueStatusManager {
|
||||
|
||||
/// The current status of the download queue.
|
||||
/// - Important
|
||||
/// Only returns meaningful values once `beginObservingIfNecessary` has been called.
|
||||
func currentStatus(for mode: BackupAttachmentDownloadQueueMode) -> BackupAttachmentDownloadQueueStatus
|
||||
|
||||
/// The current status of the download queue, and a token.
|
||||
/// - Important
|
||||
/// Only returns meaningful values once `beginObservingIfNecessary` has been called.
|
||||
func currentStatusAndToken(for mode: BackupAttachmentDownloadQueueMode) -> (BackupAttachmentDownloadQueueStatus, BackupAttachmentDownloadQueueStatusToken)
|
||||
|
||||
/// Begin observing status updates, if necessary.
|
||||
func beginObservingIfNecessary(for mode: BackupAttachmentDownloadQueueMode) -> BackupAttachmentDownloadQueueStatus
|
||||
@ -112,6 +96,13 @@ public protocol BackupAttachmentDownloadQueueStatusManager: BackupAttachmentDown
|
||||
mode: BackupAttachmentDownloadQueueMode,
|
||||
) async
|
||||
|
||||
/// Synchronously returns the minimum required disk space for downloads.
|
||||
nonisolated func minimumRequiredDiskSpaceToCompleteDownloads() -> UInt64
|
||||
|
||||
/// Check available disk space, optionally clearing in-memory state
|
||||
/// regarding past "out of space" errors.
|
||||
func checkAvailableDiskSpace(clearPreviousOutOfSpaceErrors: Bool)
|
||||
|
||||
/// Call when the download queue is emptied.
|
||||
func didEmptyQueue(for mode: BackupAttachmentDownloadQueueMode)
|
||||
|
||||
@ -123,8 +114,6 @@ public protocol BackupAttachmentDownloadQueueStatusManager: BackupAttachmentDown
|
||||
@MainActor
|
||||
class BackupAttachmentDownloadQueueStatusManagerImpl: BackupAttachmentDownloadQueueStatusManager {
|
||||
|
||||
// MARK: - BackupAttachmentDownloadQueueStatusReporter
|
||||
|
||||
func currentStatus(for mode: BackupAttachmentDownloadQueueMode) -> BackupAttachmentDownloadQueueStatus {
|
||||
return state.asQueueStatus(mode: mode, dateProvider: dateProvider)
|
||||
}
|
||||
@ -136,20 +125,6 @@ class BackupAttachmentDownloadQueueStatusManagerImpl: BackupAttachmentDownloadQu
|
||||
)
|
||||
}
|
||||
|
||||
nonisolated func minimumRequiredDiskSpaceToCompleteDownloads() -> UInt64 {
|
||||
return getRequiredDiskSpace()
|
||||
}
|
||||
|
||||
func checkAvailableDiskSpace(clearPreviousOutOfSpaceErrors: Bool) {
|
||||
state.availableDiskSpace = getAvailableDiskSpace()
|
||||
|
||||
if clearPreviousOutOfSpaceErrors {
|
||||
state.downloadDidExperienceOutOfSpaceError = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BackupAttachmentDownloadQueueStatusManager
|
||||
|
||||
func beginObservingIfNecessary(for mode: BackupAttachmentDownloadQueueMode) -> BackupAttachmentDownloadQueueStatus {
|
||||
observeDeviceAndLocalStatesIfNecessary()
|
||||
return currentStatus(for: mode)
|
||||
@ -209,6 +184,18 @@ class BackupAttachmentDownloadQueueStatusManagerImpl: BackupAttachmentDownloadQu
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func minimumRequiredDiskSpaceToCompleteDownloads() -> UInt64 {
|
||||
return getRequiredDiskSpace()
|
||||
}
|
||||
|
||||
func checkAvailableDiskSpace(clearPreviousOutOfSpaceErrors: Bool) {
|
||||
state.availableDiskSpace = getAvailableDiskSpace()
|
||||
|
||||
if clearPreviousOutOfSpaceErrors {
|
||||
state.downloadDidExperienceOutOfSpaceError = false
|
||||
}
|
||||
}
|
||||
|
||||
func setIsMainAppAndActiveOverride(_ newValue: Bool) {
|
||||
state.isMainAppAndActiveOverride = newValue
|
||||
}
|
||||
@ -440,6 +427,13 @@ class BackupAttachmentDownloadQueueStatusManagerImpl: BackupAttachmentDownloadQu
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyStatusDidChange(for mode: BackupAttachmentDownloadQueueMode) {
|
||||
NotificationCenter.default.postOnMainThread(
|
||||
name: .backupAttachmentDownloadQueueStatusDidChange(mode: mode),
|
||||
object: nil,
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: State Observation
|
||||
|
||||
private func observeDeviceAndLocalStatesIfNecessary() {
|
||||
|
||||
@ -47,32 +47,18 @@ public extension Notification.Name {
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Reports whether we are able to upload Backup attachments, via various
|
||||
/// consolidated inputs.
|
||||
/// Tracks and reports the status of the Backup attachment upload queue.
|
||||
///
|
||||
/// `@MainActor`-isolated because most of the inputs are themselves isolated.
|
||||
///
|
||||
/// - SeeAlso `BackupAttachmentUploadTracker`
|
||||
@MainActor
|
||||
public protocol BackupAttachmentUploadQueueStatusReporter {
|
||||
public protocol BackupAttachmentUploadQueueStatusManager {
|
||||
|
||||
/// The current status of the upload queue.
|
||||
/// - Important
|
||||
/// Only returns meaningful values once `beginObservingIfNecessary` has been called.
|
||||
func currentStatus(for mode: BackupAttachmentUploadQueueMode) -> BackupAttachmentUploadQueueStatus
|
||||
}
|
||||
|
||||
extension BackupAttachmentUploadQueueStatusReporter {
|
||||
fileprivate func notifyStatusDidChange(for mode: BackupAttachmentUploadQueueMode) {
|
||||
NotificationCenter.default.postOnMainThread(
|
||||
name: .backupAttachmentUploadQueueStatusDidChange(for: mode),
|
||||
object: nil,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// API for callers to manage the `StatusReporter` in response to relevant
|
||||
/// external events.
|
||||
@MainActor
|
||||
public protocol BackupAttachmentUploadQueueStatusManager: BackupAttachmentUploadQueueStatusReporter {
|
||||
|
||||
/// Begin observing status updates, if necessary.
|
||||
func beginObservingIfNecessary(for mode: BackupAttachmentUploadQueueMode) -> BackupAttachmentUploadQueueStatus
|
||||
@ -88,14 +74,10 @@ public protocol BackupAttachmentUploadQueueStatusManager: BackupAttachmentUpload
|
||||
@MainActor
|
||||
class BackupAttachmentUploadQueueStatusManagerImpl: BackupAttachmentUploadQueueStatusManager {
|
||||
|
||||
// MARK: - BackupAttachmentUploadQueueStatusReporter
|
||||
|
||||
func currentStatus(for mode: BackupAttachmentUploadQueueMode) -> BackupAttachmentUploadQueueStatus {
|
||||
return state.asQueueStatus(for: mode)
|
||||
}
|
||||
|
||||
// MARK: - BackupAttachmentUploadQueueStatusManager
|
||||
|
||||
func beginObservingIfNecessary(for mode: BackupAttachmentUploadQueueMode) -> BackupAttachmentUploadQueueStatus {
|
||||
observeDeviceAndLocalStatesIfNecessary()
|
||||
return currentStatus(for: mode)
|
||||
@ -298,6 +280,13 @@ class BackupAttachmentUploadQueueStatusManagerImpl: BackupAttachmentUploadQueueS
|
||||
|
||||
private var state: State {
|
||||
didSet {
|
||||
func notifyStatusDidChange(for mode: BackupAttachmentUploadQueueMode) {
|
||||
NotificationCenter.default.postOnMainThread(
|
||||
name: .backupAttachmentUploadQueueStatusDidChange(for: mode),
|
||||
object: nil,
|
||||
)
|
||||
}
|
||||
|
||||
if oldValue.asQueueStatus(for: .fullsize) != state.asQueueStatus(for: .fullsize) {
|
||||
notifyStatusDidChange(for: .fullsize)
|
||||
}
|
||||
|
||||
@ -17,18 +17,24 @@ public struct BackupExportJobStore {
|
||||
|
||||
// MARK: -
|
||||
|
||||
public func wipe(tx: DBWriteTransaction) {
|
||||
kvStore.removeValue(forKey: Keys.resumptionPoint, tx: tx)
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Represents a point at which an interrupted `BackupExportJob` can be
|
||||
/// resumed.
|
||||
public enum ResumptionPoint: Int64 {
|
||||
/// The job should be resumed from the beginning.
|
||||
case beginning
|
||||
case beginning = 0
|
||||
/// The job should be resumed after Backup-file-related stages.
|
||||
case postBackupFile
|
||||
case postBackupFile = 1
|
||||
}
|
||||
|
||||
public func lastReachedResumptionPoint(tx: DBReadTransaction) -> ResumptionPoint? {
|
||||
return kvStore.fetchValue(Int64.self, forKey: Keys.resumptionPoint, tx: tx)
|
||||
.flatMap { ResumptionPoint(rawValue: $0) }
|
||||
.map { ResumptionPoint(rawValue: $0).owsFailUnwrap("Unexpected value: \($0)") }
|
||||
}
|
||||
|
||||
public func setReachedResumptionPoint(_ point: ResumptionPoint?, tx: DBWriteTransaction) {
|
||||
|
||||
@ -393,6 +393,7 @@ extension AppSetup.GlobalsContinuation {
|
||||
)
|
||||
|
||||
let backupNonceMetadataStore = BackupNonceMetadataStore()
|
||||
let backupExportJobStore = BackupExportJobStore()
|
||||
let backupSettingsStore = BackupSettingsStore()
|
||||
let accountKeyStore = AccountKeyStore(
|
||||
backupSettingsStore: backupSettingsStore,
|
||||
@ -1611,7 +1612,6 @@ extension AppSetup.GlobalsContinuation {
|
||||
receiptSender: receiptSender,
|
||||
)
|
||||
|
||||
let backupExportJobStore = BackupExportJobStore()
|
||||
let backupExportJob = BackupExportJobImpl(
|
||||
accountKeyStore: accountKeyStore,
|
||||
backupArchiveManager: backupArchiveManager,
|
||||
|
||||
@ -48,3 +48,20 @@ extension NotificationCenter {
|
||||
removeObserver(observer.wrapped)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension NotificationCenter {
|
||||
public func startTaskTrackingNotifications(
|
||||
named name: Notification.Name,
|
||||
onNotification: @MainActor @escaping () -> Void,
|
||||
) -> Task<Void, Never> {
|
||||
return Task.detached {
|
||||
for await _ in NotificationCenter.default.notifications(named: name) {
|
||||
await MainActor.run {
|
||||
onNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user