Music_Player3/Pods/Tiercel/Sources/General/SessionManager.swift
2024-06-03 09:48:39 +08:00

1010 lines
36 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// SessionManager.swift
// Tiercel
//
// Created by Daniels on 2018/3/16.
// Copyright © 2018 Daniels. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
public class SessionManager {
enum MaintainTasksAction {
case append(DownloadTask)
case remove(DownloadTask)
case succeeded(DownloadTask)
case appendRunningTasks(DownloadTask)
case removeRunningTasks(DownloadTask)
}
public let operationQueue: DispatchQueue
public let cache: Cache
public let identifier: String
public var completionHandler: (() -> Void)?
public var configuration: SessionConfiguration {
get { protectedState.wrappedValue.configuration }
set {
operationQueue.sync {
protectedState.write {
$0.configuration = newValue
if $0.status == .running {
totalSuspend()
}
}
}
}
}
private struct State {
var logger: Logable
var isControlNetworkActivityIndicator: Bool = true
var configuration: SessionConfiguration {
didSet {
guard !shouldCreatSession else { return }
shouldCreatSession = true
if status == .running {
if configuration.maxConcurrentTasksLimit <= oldValue.maxConcurrentTasksLimit {
restartTasks = runningTasks + tasks.filter { $0.status == .waiting }
} else {
restartTasks = tasks.filter { $0.status == .waiting || $0.status == .running }
}
} else {
session?.invalidateAndCancel()
session = nil
}
}
}
var session: URLSession?
var shouldCreatSession: Bool = false
var timer: DispatchSourceTimer?
var status: Status = .waiting
var tasks: [DownloadTask] = []
var taskMapper: [String: DownloadTask] = [String: DownloadTask]()
var urlMapper: [URL: URL] = [URL: URL]()
var runningTasks: [DownloadTask] = []
var restartTasks: [DownloadTask] = []
var succeededTasks: [DownloadTask] = []
var speed: Int64 = 0
var timeRemaining: Int64 = 0
var progressExecuter: Executer<SessionManager>?
var successExecuter: Executer<SessionManager>?
var failureExecuter: Executer<SessionManager>?
var completionExecuter: Executer<SessionManager>?
var controlExecuter: Executer<SessionManager>?
}
private let protectedState: Protected<State>
public var logger: Logable {
get { protectedState.wrappedValue.logger }
set { protectedState.write { $0.logger = newValue } }
}
public var isControlNetworkActivityIndicator: Bool {
get { protectedState.wrappedValue.isControlNetworkActivityIndicator }
set { protectedState.write { $0.isControlNetworkActivityIndicator = newValue } }
}
internal var shouldRun: Bool {
return runningTasks.count < configuration.maxConcurrentTasksLimit
}
private var session: URLSession? {
get { protectedState.wrappedValue.session }
set { protectedState.write { $0.session = newValue } }
}
private var shouldCreatSession: Bool {
get { protectedState.wrappedValue.shouldCreatSession }
set { protectedState.write { $0.shouldCreatSession = newValue } }
}
private var timer: DispatchSourceTimer? {
get { protectedState.wrappedValue.timer }
set { protectedState.write { $0.timer = newValue } }
}
public private(set) var status: Status {
get { protectedState.wrappedValue.status }
set {
protectedState.write { $0.status = newValue }
if newValue == .willSuspend || newValue == .willCancel || newValue == .willRemove {
return
}
log(.sessionManager(newValue.rawValue, manager: self))
}
}
public private(set) var tasks: [DownloadTask] {
get { protectedState.wrappedValue.tasks }
set { protectedState.write { $0.tasks = newValue } }
}
private var runningTasks: [DownloadTask] {
get { protectedState.wrappedValue.runningTasks }
set { protectedState.write { $0.runningTasks = newValue } }
}
private var restartTasks: [DownloadTask] {
get { protectedState.wrappedValue.restartTasks }
set { protectedState.write { $0.restartTasks = newValue } }
}
public private(set) var succeededTasks: [DownloadTask] {
get { protectedState.wrappedValue.succeededTasks }
set { protectedState.write { $0.succeededTasks = newValue } }
}
private let _progress = Progress()
public var progress: Progress {
_progress.completedUnitCount = tasks.reduce(0, { $0 + $1.progress.completedUnitCount })
_progress.totalUnitCount = tasks.reduce(0, { $0 + $1.progress.totalUnitCount })
return _progress
}
public private(set) var speed: Int64 {
get { protectedState.wrappedValue.speed }
set { protectedState.write { $0.speed = newValue } }
}
public var speedString: String {
speed.tr.convertSpeedToString()
}
public private(set) var timeRemaining: Int64 {
get { protectedState.wrappedValue.timeRemaining }
set { protectedState.write { $0.timeRemaining = newValue } }
}
public var timeRemainingString: String {
timeRemaining.tr.convertTimeToString()
}
private var progressExecuter: Executer<SessionManager>? {
get { protectedState.wrappedValue.progressExecuter }
set { protectedState.write { $0.progressExecuter = newValue } }
}
private var successExecuter: Executer<SessionManager>? {
get { protectedState.wrappedValue.successExecuter }
set { protectedState.write { $0.successExecuter = newValue } }
}
private var failureExecuter: Executer<SessionManager>? {
get { protectedState.wrappedValue.failureExecuter }
set { protectedState.write { $0.failureExecuter = newValue } }
}
private var completionExecuter: Executer<SessionManager>? {
get { protectedState.wrappedValue.completionExecuter }
set { protectedState.write { $0.completionExecuter = newValue } }
}
private var controlExecuter: Executer<SessionManager>? {
get { protectedState.wrappedValue.controlExecuter }
set { protectedState.write { $0.controlExecuter = newValue } }
}
public init(_ identifier: String,
configuration: SessionConfiguration,
logger: Logable? = nil,
cache: Cache? = nil,
operationQueue: DispatchQueue = DispatchQueue(label: "com.Tiercel.SessionManager.operationQueue",
autoreleaseFrequency: .workItem)) {
let bundleIdentifier = Bundle.main.bundleIdentifier ?? "com.Daniels.Tiercel"
self.identifier = "\(bundleIdentifier).\(identifier)"
protectedState = Protected(
State(logger: logger ?? Logger(identifier: "\(bundleIdentifier).\(identifier)", option: .default),
configuration: configuration)
)
self.operationQueue = operationQueue
self.cache = cache ?? Cache(identifier)
self.cache.manager = self
self.cache.retrieveAllTasks().forEach { maintainTasks(with: .append($0)) }
succeededTasks = tasks.filter { $0.status == .succeeded }
log(.sessionManager("retrieveTasks", manager: self))
protectedState.write { state in
state.tasks.forEach {
$0.manager = self
$0.operationQueue = operationQueue
state.urlMapper[$0.currentURL] = $0.url
}
state.shouldCreatSession = true
}
operationQueue.sync {
createSession()
restoreStatus()
}
}
deinit {
invalidate()
}
public func invalidate() {
session?.invalidateAndCancel()
session = nil
cache.invalidate()
invalidateTimer()
}
private func createSession(_ completion: (() -> ())? = nil) {
guard shouldCreatSession else { return }
let sessionConfiguration = URLSessionConfiguration.background(withIdentifier: identifier)
sessionConfiguration.timeoutIntervalForRequest = configuration.timeoutIntervalForRequest
sessionConfiguration.httpMaximumConnectionsPerHost = 100000
sessionConfiguration.allowsCellularAccess = configuration.allowsCellularAccess
if #available(iOS 13, macOS 10.15, *) {
sessionConfiguration.allowsConstrainedNetworkAccess = configuration.allowsConstrainedNetworkAccess
sessionConfiguration.allowsExpensiveNetworkAccess = configuration.allowsExpensiveNetworkAccess
}
let sessionDelegate = SessionDelegate()
sessionDelegate.manager = self
let delegateQueue = OperationQueue(maxConcurrentOperationCount: 1,
underlyingQueue: operationQueue,
name: "com.Tiercel.SessionManager.delegateQueue")
protectedState.write {
let session = URLSession(configuration: sessionConfiguration,
delegate: sessionDelegate,
delegateQueue: delegateQueue)
$0.session = session
$0.tasks.forEach { $0.session = session }
$0.shouldCreatSession = false
}
completion?()
}
}
// MARK: - download
extension SessionManager {
///
///
/// - Parameters:
/// - url: URLConvertible
/// - headers: headers
/// - fileName: nilurlmd5
/// - Returns: urltaskurlnil
@discardableResult
public func download(_ url: URLConvertible,
headers: [String: String]? = nil,
fileName: String? = nil,
onMainQueue: Bool = true,
handler: Handler<DownloadTask>? = nil) -> DownloadTask? {
do {
let validURL = try url.asURL()
var task: DownloadTask!
operationQueue.sync {
task = fetchTask(validURL)
if let task = task {
task.update(headers, newFileName: fileName)
} else {
task = DownloadTask(validURL,
headers: headers,
fileName: fileName,
cache: cache,
operationQueue: operationQueue)
task.manager = self
task.session = session
maintainTasks(with: .append(task))
}
storeTasks()
start(task, onMainQueue: onMainQueue, handler: handler)
}
return task
} catch {
log(.error("create dowloadTask failed", error: error))
return nil
}
}
/// ,
///
/// - Parameters:
/// - urls: [URLConvertible]
/// - headers: headers
/// - fileNames: nilurlmd5
/// - Returns: urlurltask
@discardableResult
public func multiDownload(_ urls: [URLConvertible],
headersArray: [[String: String]]? = nil,
fileNames: [String]? = nil,
onMainQueue: Bool = true,
handler: Handler<SessionManager>? = nil) -> [DownloadTask] {
if let headersArray = headersArray,
headersArray.count != 0 && headersArray.count != urls.count {
log(.error("create multiple dowloadTasks failed", error: TiercelError.headersMatchFailed))
return [DownloadTask]()
}
if let fileNames = fileNames,
fileNames.count != 0 && fileNames.count != urls.count {
log(.error("create multiple dowloadTasks failed", error: TiercelError.fileNamesMatchFailed))
return [DownloadTask]()
}
var urlSet = Set<URL>()
var uniqueTasks = [DownloadTask]()
operationQueue.sync {
for (index, url) in urls.enumerated() {
let fileName = fileNames?.safeObject(at: index)
let headers = headersArray?.safeObject(at: index)
guard let validURL = try? url.asURL() else {
log(.error("create dowloadTask failed", error: TiercelError.invalidURL(url: url)))
continue
}
guard urlSet.insert(validURL).inserted else {
log(.error("create dowloadTask failed", error: TiercelError.duplicateURL(url: url)))
continue
}
var task: DownloadTask!
task = fetchTask(validURL)
if let task = task {
task.update(headers, newFileName: fileName)
} else {
task = DownloadTask(validURL,
headers: headers,
fileName: fileName,
cache: cache,
operationQueue: operationQueue)
task.manager = self
task.session = session
maintainTasks(with: .append(task))
}
uniqueTasks.append(task)
}
storeTasks()
Executer(onMainQueue: onMainQueue, handler: handler).execute(self)
operationQueue.async {
uniqueTasks.forEach {
if $0.status != .succeeded {
self._start($0)
}
}
}
}
return uniqueTasks
}
}
// MARK: - single task control
extension SessionManager {
public func fetchTask(_ url: URLConvertible) -> DownloadTask? {
do {
let validURL = try url.asURL()
return protectedState.read { $0.taskMapper[validURL.absoluteString] }
} catch {
log(.error("fetch task failed", error: TiercelError.invalidURL(url: url)))
return nil
}
}
internal func mapTask(_ currentURL: URL) -> DownloadTask? {
protectedState.read {
let url = $0.urlMapper[currentURL] ?? currentURL
return $0.taskMapper[url.absoluteString]
}
}
///
/// fileName
/// tasksuccessHandler
public func start(_ url: URLConvertible, onMainQueue: Bool = true, handler: Handler<DownloadTask>? = nil) {
operationQueue.async {
self._start(url, onMainQueue: onMainQueue, handler: handler)
}
}
public func start(_ task: DownloadTask, onMainQueue: Bool = true, handler: Handler<DownloadTask>? = nil) {
operationQueue.async {
guard let _ = self.fetchTask(task.url) else {
self.log(.error("can't start downloadTask", error: TiercelError.fetchDownloadTaskFailed(url: task.url)))
return
}
self._start(task, onMainQueue: onMainQueue, handler: handler)
}
}
private func _start(_ url: URLConvertible, onMainQueue: Bool = true, handler: Handler<DownloadTask>? = nil) {
guard let task = self.fetchTask(url) else {
log(.error("can't start downloadTask", error: TiercelError.fetchDownloadTaskFailed(url: url)))
return
}
_start(task, onMainQueue: onMainQueue, handler: handler)
}
private func _start(_ task: DownloadTask, onMainQueue: Bool = true, handler: Handler<DownloadTask>? = nil) {
task.controlExecuter = Executer(onMainQueue: onMainQueue, handler: handler)
didStart()
if !shouldCreatSession {
task.download()
} else {
task.status = .suspended
if !restartTasks.contains(task) {
restartTasks.append(task)
}
}
}
/// sessionDelegate
public func suspend(_ url: URLConvertible, onMainQueue: Bool = true, handler: Handler<DownloadTask>? = nil) {
operationQueue.async {
guard let task = self.fetchTask(url) else {
self.log(.error("can't suspend downloadTask", error: TiercelError.fetchDownloadTaskFailed(url: url)))
return
}
task.suspend(onMainQueue: onMainQueue, handler: handler)
}
}
public func suspend(_ task: DownloadTask, onMainQueue: Bool = true, handler: Handler<DownloadTask>? = nil) {
operationQueue.async {
guard let _ = self.fetchTask(task.url) else {
self.log(.error("can't suspend downloadTask", error: TiercelError.fetchDownloadTaskFailed(url: task.url)))
return
}
task.suspend(onMainQueue: onMainQueue, handler: handler)
}
}
///
///
///
///
/// sessionDelegate
public func cancel(_ url: URLConvertible, onMainQueue: Bool = true, handler: Handler<DownloadTask>? = nil) {
operationQueue.async {
guard let task = self.fetchTask(url) else {
self.log(.error("can't cancel downloadTask", error: TiercelError.fetchDownloadTaskFailed(url: url)))
return
}
task.cancel(onMainQueue: onMainQueue, handler: handler)
}
}
public func cancel(_ task: DownloadTask, onMainQueue: Bool = true, handler: Handler<DownloadTask>? = nil) {
operationQueue.async {
guard let _ = self.fetchTask(task.url) else {
self.log(.error("can't cancel downloadTask", error: TiercelError.fetchDownloadTaskFailed(url: task.url)))
return
}
task.cancel(onMainQueue: onMainQueue, handler: handler)
}
}
///
///
///
///
/// sessionDelegate
///
/// - Parameters:
/// - url: URLConvertible
/// - completely:
public func remove(_ url: URLConvertible, completely: Bool = false, onMainQueue: Bool = true, handler: Handler<DownloadTask>? = nil) {
operationQueue.async {
guard let task = self.fetchTask(url) else {
self.log(.error("can't remove downloadTask", error: TiercelError.fetchDownloadTaskFailed(url: url)))
return
}
task.remove(completely: completely, onMainQueue: onMainQueue, handler: handler)
}
}
public func remove(_ task: DownloadTask, completely: Bool = false, onMainQueue: Bool = true, handler: Handler<DownloadTask>? = nil) {
operationQueue.async {
guard let _ = self.fetchTask(task.url) else {
self.log(.error("can't remove downloadTask", error: TiercelError.fetchDownloadTaskFailed(url: task.url)))
return
}
task.remove(completely: completely, onMainQueue: onMainQueue, handler: handler)
}
}
public func moveTask(at sourceIndex: Int, to destinationIndex: Int) {
operationQueue.sync {
let range = (0..<tasks.count)
guard range.contains(sourceIndex) && range.contains(destinationIndex) else {
log(.error("move task failed, sourceIndex: \(sourceIndex), destinationIndex: \(destinationIndex)",
error: TiercelError.indexOutOfRange))
return
}
if sourceIndex == destinationIndex {
return
}
protectedState.write {
let task = $0.tasks[sourceIndex]
$0.tasks.remove(at: sourceIndex)
$0.tasks.insert(task, at: destinationIndex)
}
}
}
}
// MARK: - total tasks control
extension SessionManager {
public func totalStart(onMainQueue: Bool = true, handler: Handler<SessionManager>? = nil) {
operationQueue.async {
self.tasks.forEach { task in
if task.status != .succeeded {
self._start(task)
}
}
Executer(onMainQueue: onMainQueue, handler: handler).execute(self)
}
}
public func totalSuspend(onMainQueue: Bool = true, handler: Handler<SessionManager>? = nil) {
operationQueue.async {
guard self.status == .running || self.status == .waiting else { return }
self.status = .willSuspend
self.controlExecuter = Executer(onMainQueue: onMainQueue, handler: handler)
self.tasks.forEach { $0.suspend() }
}
}
public func totalCancel(onMainQueue: Bool = true, handler: Handler<SessionManager>? = nil) {
operationQueue.async {
guard self.status != .succeeded && self.status != .canceled else { return }
self.status = .willCancel
self.controlExecuter = Executer(onMainQueue: onMainQueue, handler: handler)
self.tasks.forEach { $0.cancel() }
}
}
public func totalRemove(completely: Bool = false, onMainQueue: Bool = true, handler: Handler<SessionManager>? = nil) {
operationQueue.async {
guard self.status != .removed else { return }
self.status = .willRemove
self.controlExecuter = Executer(onMainQueue: onMainQueue, handler: handler)
self.tasks.forEach { $0.remove(completely: completely) }
}
}
public func tasksSort(by areInIncreasingOrder: (DownloadTask, DownloadTask) throws -> Bool) rethrows {
try operationQueue.sync {
try protectedState.write {
try $0.tasks.sort(by: areInIncreasingOrder)
}
}
}
}
// MARK: - status handle
extension SessionManager {
internal func maintainTasks(with action: MaintainTasksAction) {
switch action {
case let .append(task):
protectedState.write { state in
state.tasks.append(task)
state.taskMapper[task.url.absoluteString] = task
state.urlMapper[task.currentURL] = task.url
}
case let .remove(task):
protectedState.write { state in
if state.status == .willRemove {
state.taskMapper.removeValue(forKey: task.url.absoluteString)
state.urlMapper.removeValue(forKey: task.currentURL)
if state.taskMapper.values.isEmpty {
state.tasks.removeAll()
state.succeededTasks.removeAll()
}
} else if state.status == .willCancel {
state.taskMapper.removeValue(forKey: task.url.absoluteString)
state.urlMapper.removeValue(forKey: task.currentURL)
if state.taskMapper.values.count == state.succeededTasks.count {
state.tasks = state.succeededTasks
}
} else {
state.taskMapper.removeValue(forKey: task.url.absoluteString)
state.urlMapper.removeValue(forKey: task.currentURL)
state.tasks.removeAll {
$0.url.absoluteString == task.url.absoluteString
}
if task.status == .removed {
state.succeededTasks.removeAll {
$0.url.absoluteString == task.url.absoluteString
}
}
}
}
case let .succeeded(task):
succeededTasks.append(task)
case let .appendRunningTasks(task):
protectedState.write { state in
state.runningTasks.append(task)
}
case let .removeRunningTasks(task):
protectedState.write { state in
state.runningTasks.removeAll {
$0.url.absoluteString == task.url.absoluteString
}
}
}
}
internal func updateUrlMapper(with task: DownloadTask) {
protectedState.write { $0.urlMapper[task.currentURL] = task.url }
}
private func restoreStatus() {
if self.tasks.isEmpty {
return
}
session?.getTasksWithCompletionHandler { [weak self] (dataTasks, uploadTasks, downloadTasks) in
guard let self = self else { return }
downloadTasks.forEach { downloadTask in
if downloadTask.state == .running,
let currentURL = downloadTask.currentRequest?.url,
let task = self.mapTask(currentURL) {
self.didStart()
self.maintainTasks(with: .appendRunningTasks(task))
task.status = .running
task.sessionTask = downloadTask
}
}
self.storeTasks()
// mananger
if !self.shouldComplete() {
self.shouldSuspend()
}
}
}
private func shouldComplete() -> Bool {
let isSucceeded = self.tasks.allSatisfy { $0.status == .succeeded }
let isCompleted = isSucceeded ? isSucceeded :
self.tasks.allSatisfy { $0.status == .succeeded || $0.status == .failed }
guard isCompleted else { return false }
if status == .succeeded || status == .failed {
return true
}
timeRemaining = 0
progressExecuter?.execute(self)
status = isSucceeded ? .succeeded : .failed
executeCompletion(isSucceeded)
return true
}
private func shouldSuspend() {
let isSuspended = tasks.allSatisfy { $0.status == .suspended || $0.status == .succeeded || $0.status == .failed }
if isSuspended {
if status == .suspended {
return
}
status = .suspended
executeControl()
executeCompletion(false)
if shouldCreatSession {
session?.invalidateAndCancel()
session = nil
}
}
}
internal func didStart() {
if status != .running {
createTimer()
status = .running
progressExecuter?.execute(self)
}
}
internal func updateProgress() {
if isControlNetworkActivityIndicator {
DispatchQueue.tr.executeOnMain {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
}
}
progressExecuter?.execute(self)
NotificationCenter.default.postNotification(name: SessionManager.runningNotification, sessionManager: self)
}
internal func didCancelOrRemove(_ task: DownloadTask) {
maintainTasks(with: .remove(task))
// 使taskmanager
if tasks.isEmpty {
if task.status == .canceled {
status = .willCancel
}
if task.status == .removed {
status = .willRemove
}
}
}
internal func storeTasks() {
cache.storeTasks(tasks)
}
internal func determineStatus(fromRunningTask: Bool) {
if isControlNetworkActivityIndicator {
DispatchQueue.tr.executeOnMain {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}
}
// removed
if status == .willRemove {
if tasks.isEmpty {
status = .removed
executeControl()
ending(false)
}
return
}
// canceled
if status == .willCancel {
let succeededTasksCount = protectedState.wrappedValue.taskMapper.values.count
if tasks.count == succeededTasksCount {
status = .canceled
executeControl()
ending(false)
return
}
return
}
// completed
let isCompleted = tasks.allSatisfy { $0.status == .succeeded || $0.status == .failed }
if isCompleted {
if status == .succeeded || status == .failed {
storeTasks()
return
}
timeRemaining = 0
progressExecuter?.execute(self)
let isSucceeded = tasks.allSatisfy { $0.status == .succeeded }
status = isSucceeded ? .succeeded : .failed
ending(isSucceeded)
return
}
// suspended
let isSuspended = tasks.allSatisfy { $0.status == .suspended ||
$0.status == .succeeded ||
$0.status == .failed }
if isSuspended {
if status == .suspended {
storeTasks()
return
}
status = .suspended
if shouldCreatSession {
session?.invalidateAndCancel()
session = nil
} else {
executeControl()
ending(false)
}
return
}
if status == .willSuspend {
return
}
storeTasks()
if fromRunningTask {
// next task
operationQueue.async {
self.startNextTask()
}
}
}
private func ending(_ isSucceeded: Bool) {
executeCompletion(isSucceeded)
storeTasks()
invalidateTimer()
}
private func startNextTask() {
guard let waitingTask = tasks.first (where: { $0.status == .waiting }) else { return }
waitingTask.download()
}
}
// MARK: - info
extension SessionManager {
static let refreshInterval: Double = 1
private func createTimer() {
if timer == nil {
timer = DispatchSource.makeTimerSource(flags: .strict, queue: operationQueue)
timer?.schedule(deadline: .now(), repeating: Self.refreshInterval)
timer?.setEventHandler(handler: { [weak self] in
guard let self = self else { return }
self.updateSpeedAndTimeRemaining()
})
timer?.resume()
}
}
private func invalidateTimer() {
timer?.cancel()
timer = nil
}
internal func updateSpeedAndTimeRemaining() {
let speed = runningTasks.reduce(Int64(0), {
$1.updateSpeedAndTimeRemaining()
return $0 + $1.speed
})
updateTimeRemaining(speed)
}
private func updateTimeRemaining(_ speed: Int64) {
var timeRemaining: Double
if speed != 0 {
timeRemaining = (Double(progress.totalUnitCount) - Double(progress.completedUnitCount)) / Double(speed)
if timeRemaining >= 0.8 && timeRemaining < 1 {
timeRemaining += 1
}
} else {
timeRemaining = 0
}
protectedState.write {
$0.speed = speed
$0.timeRemaining = Int64(timeRemaining)
}
}
internal func log(_ type: LogType) {
logger.log(type)
}
}
// MARK: - closure
extension SessionManager {
@discardableResult
public func progress(onMainQueue: Bool = true, handler: @escaping Handler<SessionManager>) -> Self {
progressExecuter = Executer(onMainQueue: onMainQueue, handler: handler)
return self
}
@discardableResult
public func success(onMainQueue: Bool = true, handler: @escaping Handler<SessionManager>) -> Self {
successExecuter = Executer(onMainQueue: onMainQueue, handler: handler)
if status == .succeeded && completionExecuter == nil{
operationQueue.async {
self.successExecuter?.execute(self)
}
}
return self
}
@discardableResult
public func failure(onMainQueue: Bool = true, handler: @escaping Handler<SessionManager>) -> Self {
failureExecuter = Executer(onMainQueue: onMainQueue, handler: handler)
if completionExecuter == nil &&
(status == .suspended ||
status == .canceled ||
status == .removed ||
status == .failed) {
operationQueue.async {
self.failureExecuter?.execute(self)
}
}
return self
}
@discardableResult
public func completion(onMainQueue: Bool = true, handler: @escaping Handler<SessionManager>) -> Self {
completionExecuter = Executer(onMainQueue: onMainQueue, handler: handler)
if status == .suspended ||
status == .canceled ||
status == .removed ||
status == .succeeded ||
status == .failed {
operationQueue.async {
self.completionExecuter?.execute(self)
}
}
return self
}
private func executeCompletion(_ isSucceeded: Bool) {
if let completionExecuter = completionExecuter {
completionExecuter.execute(self)
} else if isSucceeded {
successExecuter?.execute(self)
} else {
failureExecuter?.execute(self)
}
NotificationCenter.default.postNotification(name: SessionManager.didCompleteNotification, sessionManager: self)
}
private func executeControl() {
controlExecuter?.execute(self)
controlExecuter = nil
}
}
// MARK: - call back
extension SessionManager {
internal func didBecomeInvalidation(withError error: Error?) {
createSession { [weak self] in
guard let self = self else { return }
self.restartTasks.forEach { self._start($0) }
self.restartTasks.removeAll()
}
}
internal func didFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.tr.executeOnMain {
self.completionHandler?()
self.completionHandler = nil
}
}
}