Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions Feather.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,6 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Feather/Resources/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Feather;
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = NO;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright (c) 2024 Samara M";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
Expand Down Expand Up @@ -418,15 +417,15 @@
SDKROOT = auto;
STRIP_INSTALLED_PRODUCT = NO;
STRIP_PNG_TEXT = NO;
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Feather/Supporting Files/Feather-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,3";
TARGETED_DEVICE_FAMILY = "1,2";
TVOS_DEPLOYMENT_TARGET = 16.0;
XROS_DEPLOYMENT_TARGET = 2.2;
};
Expand Down Expand Up @@ -454,7 +453,6 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Feather/Resources/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Feather;
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = NO;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright (c) 2024 Samara M";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
Expand Down Expand Up @@ -482,14 +480,14 @@
SDKROOT = auto;
STRIPFLAGS = "-rSTx";
STRIP_PNG_TEXT = NO;
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Feather/Supporting Files/Feather-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,3";
TARGETED_DEVICE_FAMILY = "1,2";
TVOS_DEPLOYMENT_TARGET = 16.0;
XROS_DEPLOYMENT_TARGET = 2.2;
};
Expand Down
63 changes: 45 additions & 18 deletions Feather/Backend/Observable/DownloadManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ class Download: Identifiable, @unchecked Sendable {

var overallProgress: Double {
onlyArchiving
? unpackageProgress
: (0.3 * unpackageProgress) + (0.7 * progress)
? unpackageProgress
: (0.3 * unpackageProgress) + (0.7 * progress)
}

var task: URLSessionDownloadTask?
Expand All @@ -29,7 +29,7 @@ class Download: Identifiable, @unchecked Sendable {
let url: URL
let fileName: String
let onlyArchiving: Bool
init(
id: String,
url: URL,
Expand All @@ -52,7 +52,8 @@ class DownloadManager: NSObject, ObservableObject {
}

private var _session: URLSession!


#if !targetEnvironment(macCatalyst)
private func _updateBackgroundAudioState() {
if #unavailable(iOS 26.0){
if !downloads.isEmpty {
Expand All @@ -62,13 +63,14 @@ class DownloadManager: NSObject, ObservableObject {
}
}
}

#endif

override init() {
super.init()
let configuration = URLSessionConfiguration.default
_session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}
func startDownload(
from url: URL,
id: String = UUID().uuidString
Expand All @@ -77,19 +79,23 @@ class DownloadManager: NSObject, ObservableObject {
resumeDownload(existingDownload)
return existingDownload
}
let download = Download(id: id, url: url)
let task = _session.downloadTask(with: url)
download.task = task
task.resume()
downloads.append(download)

#if !targetEnvironment(macCatalyst)
if #available(iOS 26.0, *) {
BackgroundTaskManager.shared.startTask(for: id, filename: url.lastPathComponent)
} else {
_updateBackgroundAudioState()
}
#endif

return download
}

Expand All @@ -99,36 +105,50 @@ class DownloadManager: NSObject, ObservableObject {
) -> Download {
let download = Download(id: id, url: url, onlyArchiving: true)
downloads.append(download)

#if !targetEnvironment(macCatalyst)
_updateBackgroundAudioState()
#endif

return download
}
func resumeDownload(_ download: Download) {
if let resumeData = download.resumeData {
let task = _session.downloadTask(withResumeData: resumeData)
download.task = task
task.resume()

#if !targetEnvironment(macCatalyst)
_updateBackgroundAudioState()
#endif
} else if let url = download.task?.originalRequest?.url {
let task = _session.downloadTask(with: url)
download.task = task
task.resume()

#if !targetEnvironment(macCatalyst)
_updateBackgroundAudioState()
#endif
}
}
func cancelDownload(_ download: Download) {
download.task?.cancel()
if let index = downloads.firstIndex(where: { $0.id == download.id }) {
downloads.remove(at: index)

#if !targetEnvironment(macCatalyst)
_updateBackgroundAudioState()

if #available(iOS 26.0, *) {
BackgroundTaskManager.shared.stopTask(for: download.id, success: false)
}
#endif
}
}
func isManualDownload(_ string: String) -> Bool {
return string.contains("FeatherManualDownload")
}
Expand Down Expand Up @@ -158,10 +178,14 @@ extension DownloadManager: URLSessionDownloadDelegate {
DispatchQueue.main.async {
if let index = DownloadManager.shared.getDownloadIndex(by: dl.id) {
DownloadManager.shared.downloads.remove(at: index)

#if !targetEnvironment(macCatalyst)
if #available(iOS 26.0, *) {
BackgroundTaskManager.shared.updateProgress(for: dl.id, progress: 1.0)
}

self._updateBackgroundAudioState()
#endif
}
}
}
Expand All @@ -188,22 +212,25 @@ extension DownloadManager: URLSessionDownloadDelegate {
print("Error handling downloaded file: \(error.localizedDescription)")
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
guard let download = getDownloadTask(by: downloadTask) else { return }
DispatchQueue.main.async {
download.progress = totalBytesExpectedToWrite > 0
? Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
: 0
? Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
: 0
download.bytesDownloaded = totalBytesWritten
download.totalBytes = totalBytesExpectedToWrite

#if !targetEnvironment(macCatalyst)
if #available(iOS 26.0, *) {
BackgroundTaskManager.shared.updateProgress(for: download.id, progress: download.overallProgress)
}
#endif
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard
let _ = error,
Expand Down
14 changes: 9 additions & 5 deletions Feather/Utilities/BackgroundAudioManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@
// Created by Nagata Asami on 12/10/25.
//

#if !targetEnvironment(macCatalyst)

import AVFoundation

class BackgroundAudioManager {
static let shared = BackgroundAudioManager()
private let _engine = AVAudioEngine()

private init() {}

func start() {
do {
let session = AVAudioSession.sharedInstance()

try session.setCategory(.playback, options: [.mixWithOthers])
try session.setActive(true)
let silence = AVAudioSourceNode { _, _, frameCount, audioBufferList -> OSStatus in
Expand All @@ -27,17 +29,19 @@ class BackgroundAudioManager {
}
return noErr
}

_engine.attach(silence)
_engine.connect(silence, to: _engine.mainMixerNode, format: nil)
try _engine.start()
} catch {
print("failed to start engine:", error)
}
}

func stop() {
_engine.stop()
try? AVAudioSession.sharedInstance().setActive(false)
}
}

#endif
28 changes: 16 additions & 12 deletions Feather/Utilities/BackgroundTaskManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,29 @@
// Created by Nagata Asami on 4/1/26.
//

#if !targetEnvironment(macCatalyst)

import Foundation
import BackgroundTasks
import CryptoKit

@available(iOS 26.0, *)
class BackgroundTaskManager: ObservableObject {
static let shared = BackgroundTaskManager()
private let baseId = "\(Bundle.main.bundleIdentifier!).userTask"
private var activeTasks: [String: BGContinuedProcessingTask] = [:]
private var registeredTasks: Set<String> = []
func startTask(for downloadId: String, filename: String) {
let taskIdentifier = "\(baseId).\(downloadId.md5)"
if !registeredTasks.contains(taskIdentifier) {
BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil) { task in
guard let task = task as? BGContinuedProcessingTask else { return }
self.activeTasks[task.identifier] = task
task.expirationHandler = {
if let download = DownloadManager.shared.getDownload(by: downloadId) {
DownloadManager.shared.cancelDownload(download)
Expand All @@ -35,7 +37,7 @@ class BackgroundTaskManager: ObservableObject {
}
self.registeredTasks.insert(taskIdentifier)
}
let request = BGContinuedProcessingTaskRequest(identifier: taskIdentifier, title: filename, subtitle: .localized("Downloading"))
request.strategy = .queue
do {
Expand All @@ -44,25 +46,25 @@ class BackgroundTaskManager: ObservableObject {
print(error)
}
}
func updateProgress(for downloadId: String, progress: Double) {
let taskIdentifier = "\(baseId).\(downloadId.md5)"
guard let task = activeTasks[taskIdentifier] else { return }
task.progress.totalUnitCount = 100
task.progress.completedUnitCount = Int64(progress * 100)
task.updateTitle(task.title, subtitle: "\(Int(progress * 100))%")
if task.progress.completedUnitCount == task.progress.totalUnitCount {
stopTask(for: downloadId, success: true)
}
}
func stopTask(for downloadId: String, success: Bool) {
let taskIdentifier = "\(baseId).\(downloadId.md5)"
guard let task = activeTasks[taskIdentifier] else { return }
task.setTaskCompleted(success: success)
activeTasks.removeValue(forKey: taskIdentifier)
}
Expand All @@ -73,3 +75,5 @@ extension String {
Insecure.MD5.hash(data: Data(self.utf8)).map { String(format: "%02hhx", $0) }.joined()
}
}

#endif
3 changes: 3 additions & 0 deletions Feather/Utilities/Handlers/AppFileHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,12 @@ final class AppFileHandler: NSObject, @unchecked Sendable {
if let download = download {
DispatchQueue.main.async {
download.unpackageProgress = progress

#if !targetEnvironment(macCatalyst)
if #available(iOS 26.0, *) {
BackgroundTaskManager.shared.updateProgress(for: download.id, progress: download.overallProgress)
}
#endif
}
}
}
Expand Down
Loading