Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize launch process #1197

Merged
merged 3 commits into from
Oct 6, 2024
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
40 changes: 28 additions & 12 deletions BookPlayer.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

256 changes: 71 additions & 185 deletions BookPlayer/AppDelegate.swift

Large diffs are not rendered by default.

36 changes: 14 additions & 22 deletions BookPlayer/AppIntents/CustomRewindIntent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
// Copyright © 2024 Tortuga Power. All rights reserved.
//

import AppIntents
import BookPlayerKit
import Foundation
import AppIntents

@available(iOS 16.4, macOS 14.0, watchOS 10.0, tvOS 16.0, *)
struct CustomRewindIntent: AudioStartingIntent, ForegroundContinuableIntent {
@available(iOS 16.0, macOS 14.0, watchOS 10.0, tvOS 16.0, *)
struct CustomRewindIntent: AudioStartingIntent {
static var title: LocalizedStringResource = "intent_custom_skiprewind_title"

@Parameter(
Expand All @@ -24,30 +24,22 @@ struct CustomRewindIntent: AudioStartingIntent, ForegroundContinuableIntent {
Summary("Rewind \(\.$interval)")
}

func perform() async throws -> some IntentResult {
let seconds = interval.converted(to: .seconds).value
let stack = try await DatabaseInitializer().loadCoreDataStack()

let continuation: (@MainActor () async throws -> Void) = {
let actionString = CommandParser.createActionString(
from: .skipRewind,
parameters: [URLQueryItem(name: "interval", value: "\(seconds)")]
)
let actionURL = URL(string: actionString)!
UIApplication.shared.open(actionURL)
}
@Dependency
var playerLoaderService: PlayerLoaderService

guard let appDelegate = await AppDelegate.shared else {
throw needsToContinueInForegroundError(continuation: continuation)
}
@Dependency
var libraryService: LibraryService

let coreServices = await appDelegate.createCoreServicesIfNeeded(from: stack)
func perform() async throws -> some IntentResult {
let seconds = interval.converted(to: .seconds).value

guard coreServices.playerManager.hasLoadedBook() else {
throw needsToContinueInForegroundError(continuation: continuation)
if !playerLoaderService.playerManager.hasLoadedBook(),
let book = libraryService.getLastPlayedItems(limit: 1)?.first
{
try await playerLoaderService.loadPlayer(book.relativePath, autoplay: false)
}

coreServices.playerManager.skip(-seconds)
playerLoaderService.playerManager.skip(-seconds)

return .result()
}
Expand Down
36 changes: 14 additions & 22 deletions BookPlayer/AppIntents/CustomSkipForwardIntent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
// Copyright © 2024 Tortuga Power. All rights reserved.
//

import AppIntents
import BookPlayerKit
import Foundation
import AppIntents

@available(iOS 16.4, macOS 14.0, watchOS 10.0, tvOS 16.0, *)
struct CustomSkipForwardIntent: AudioStartingIntent, ForegroundContinuableIntent {
@available(iOS 16.0, macOS 14.0, watchOS 10.0, tvOS 16.0, *)
struct CustomSkipForwardIntent: AudioStartingIntent {
static var title: LocalizedStringResource = "intent_custom_skipforward_title"

@Parameter(
Expand All @@ -24,30 +24,22 @@ struct CustomSkipForwardIntent: AudioStartingIntent, ForegroundContinuableIntent
Summary("Skip forward \(\.$interval)")
}

func perform() async throws -> some IntentResult {
let seconds = interval.converted(to: .seconds).value
let stack = try await DatabaseInitializer().loadCoreDataStack()

let continuation: (@MainActor () async throws -> Void) = {
let actionString = CommandParser.createActionString(
from: .skipForward,
parameters: [URLQueryItem(name: "interval", value: "\(seconds)")]
)
let actionURL = URL(string: actionString)!
UIApplication.shared.open(actionURL)
}
@Dependency
var playerLoaderService: PlayerLoaderService

guard let appDelegate = await AppDelegate.shared else {
throw needsToContinueInForegroundError(continuation: continuation)
}
@Dependency
var libraryService: LibraryService

let coreServices = await appDelegate.createCoreServicesIfNeeded(from: stack)
func perform() async throws -> some IntentResult {
let seconds = interval.converted(to: .seconds).value

guard coreServices.playerManager.hasLoadedBook() else {
throw needsToContinueInForegroundError(continuation: continuation)
if !playerLoaderService.playerManager.hasLoadedBook(),
let book = libraryService.getLastPlayedItems(limit: 1)?.first
{
try await playerLoaderService.loadPlayer(book.relativePath, autoplay: false)
}

coreServices.playerManager.skip(seconds)
playerLoaderService.playerManager.skip(seconds)

return .result()
}
Expand Down
34 changes: 10 additions & 24 deletions BookPlayer/AppIntents/LastBookStartPlaybackIntent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,26 @@
// Copyright © 2023 Tortuga Power. All rights reserved.
//

import Foundation
import AppIntents
import BookPlayerKit
import Foundation

@available(iOS 16.4, macOS 14.0, watchOS 10.0, *)
struct LastBookStartPlaybackIntent: AudioStartingIntent, ForegroundContinuableIntent {
@available(iOS 16.0, macOS 14.0, watchOS 10.0, *)
struct LastBookStartPlaybackIntent: AudioStartingIntent {
static var title: LocalizedStringResource = "intent_lastbook_play_title"

func perform() async throws -> some IntentResult {
let stack = try await DatabaseInitializer().loadCoreDataStack()

guard let appDelegate = await AppDelegate.shared else {
throw needsToContinueInForegroundError {
let actionString = CommandParser.createActionString(
from: .play,
parameters: [URLQueryItem(name: "autoplay", value: "true")]
)
let actionURL = URL(string: actionString)!
UIApplication.shared.open(actionURL)
}
}
@Dependency
var playerLoaderService: PlayerLoaderService

let coreServices = await appDelegate.createCoreServicesIfNeeded(from: stack)
@Dependency
var libraryService: LibraryService

guard let book = coreServices.libraryService.getLastPlayedItems(limit: 1)?.first else {
func perform() async throws -> some IntentResult {
guard let book = libraryService.getLastPlayedItems(limit: 1)?.first else {
throw "intent_lastbook_empty_error".localized
}

await appDelegate.loadPlayer(
book.relativePath,
autoplay: true,
showPlayer: nil,
alertPresenter: VoidAlertPresenter()
)
try await playerLoaderService.loadPlayer(book.relativePath, autoplay: true)

return .result()
}
Expand Down
23 changes: 7 additions & 16 deletions BookPlayer/AppIntents/PausePlaybackIntent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,19 @@
// Copyright © 2023 Tortuga Power. All rights reserved.
//

import Foundation
import AppIntents
import BookPlayerKit
import Foundation

@available(iOS 16.4, macOS 14.0, watchOS 10.0, *)
struct PausePlaybackIntent: AudioStartingIntent, ForegroundContinuableIntent {
@available(iOS 16.0, macOS 14.0, watchOS 10.0, *)
struct PausePlaybackIntent: AudioStartingIntent {
static var title: LocalizedStringResource = "intent_playback_pause_title"

func perform() async throws -> some IntentResult {
let stack = try await DatabaseInitializer().loadCoreDataStack()

guard let appDelegate = await AppDelegate.shared else {
throw needsToContinueInForegroundError {
let actionString = CommandParser.createActionString(from: .pause, parameters: [])
let actionURL = URL(string: actionString)!
UIApplication.shared.open(actionURL)
}
}

let coreServices = await appDelegate.createCoreServicesIfNeeded(from: stack)
@Dependency
var playerLoaderService: PlayerLoaderService

coreServices.playerManager.pause()
func perform() async throws -> some IntentResult {
playerLoaderService.playerManager.pause()

return .result()
}
Expand Down
75 changes: 39 additions & 36 deletions BookPlayer/Coordinators/DataInitializerCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class DataInitializerCoordinator: BPLogger {
let databaseInitializer: DatabaseInitializer = DatabaseInitializer()
let alertPresenter: AlertPresenter

var onFinish: ((CoreDataStack) -> Void)?
var onFinish: (() -> Void)?

init(alertPresenter: AlertPresenter) {
self.alertPresenter = alertPresenter
Expand All @@ -28,13 +28,22 @@ class DataInitializerCoordinator: BPLogger {
}

func initializeLibrary(isRecoveryAttempt: Bool) async {
do {
let stack = try await databaseInitializer.loadCoreDataStack()
finishLibrarySetup(stack, fromRecovery: isRecoveryAttempt)
} catch let error as NSError where error.domain == NSPOSIXErrorDomain && (
error.code == ENOSPC
|| error.code == NSFileWriteOutOfSpaceError
) {
let appDelegate = await AppDelegate.shared!
_ = await appDelegate.setupCoreServicesTask?.result

if let errorCoreServicesSetup = await appDelegate.errorCoreServicesSetup {
await handleError(errorCoreServicesSetup as NSError)
return
}

await finishLibrarySetup(fromRecovery: isRecoveryAttempt)
}

func handleError(_ error: NSError) async {
if error.domain == NSPOSIXErrorDomain
&& (error.code == ENOSPC
|| error.code == NSFileWriteOutOfSpaceError)
{
// CoreData may fail if device doesn't have space
await MainActor.run {
alertPresenter.showAlert(
Expand All @@ -43,19 +52,12 @@ class DataInitializerCoordinator: BPLogger {
completion: nil
)
}
} catch let error as NSError where (
error.code == NSMigrationError ||
error.code == NSMigrationConstraintViolationError ||
error.code == NSMigrationCancelledError ||
error.code == NSMigrationMissingSourceModelError ||
error.code == NSMigrationMissingMappingModelError ||
error.code == NSMigrationManagerSourceStoreError ||
error.code == NSMigrationManagerDestinationStoreError ||
error.code == NSEntityMigrationPolicyError ||
error.code == NSValidationMultipleErrorsError ||
error.code == NSValidationMissingMandatoryPropertyError
) {
// TODO: We can handle `isRecoveryAttempt` to show a different error message
} else if error.code == NSMigrationError || error.code == NSMigrationConstraintViolationError
|| error.code == NSMigrationCancelledError || error.code == NSMigrationMissingSourceModelError
|| error.code == NSMigrationMissingMappingModelError || error.code == NSMigrationManagerSourceStoreError
|| error.code == NSMigrationManagerDestinationStoreError || error.code == NSEntityMigrationPolicyError
|| error.code == NSValidationMultipleErrorsError || error.code == NSValidationMissingMandatoryPropertyError
{
Self.logger.warning("Failed to perform migration, attempting recovery with the loading library sequence")
await MainActor.run {
alertPresenter.showAlert(
Expand All @@ -65,46 +67,46 @@ class DataInitializerCoordinator: BPLogger {
recoverLibraryFromFailedMigration()
}
}
} catch {
let error = error as NSError
} else {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}

func recoverLibraryFromFailedMigration() {
Task {
databaseInitializer.cleanupStoreFiles()
await AppDelegate.shared?.resetCoreServices()
await initializeLibrary(isRecoveryAttempt: true)
}
}

func finishLibrarySetup(_ stack: CoreDataStack, fromRecovery: Bool) {
let dataManager = DataManager(coreDataStack: stack)
let libraryService = LibraryService(dataManager: dataManager)
func finishLibrarySetup(fromRecovery: Bool) async {
let coreServices = await AppDelegate.shared!.coreServices!

setupDefaultState(
libraryService: libraryService,
dataManager: dataManager
libraryService: coreServices.libraryService,
dataManager: coreServices.dataManager
)

if fromRecovery {
let files = getLibraryFiles()
libraryService.insertItems(from: files)
coreServices.libraryService.insertItems(from: files)
}

DispatchQueue.main.async {
self.onFinish?(stack)
await MainActor.run {
self.onFinish?()
}
}

private func getLibraryFiles() -> [URL] {
let enumerator = FileManager.default.enumerator(
at: DataManager.getProcessedFolderURL(),
includingPropertiesForKeys: [.isDirectoryKey],
options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: { (url, error) -> Bool in
options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants],
errorHandler: { (url, error) -> Bool in
print("directoryEnumerator error at \(url): ", error)
return true
})!
}
)!
var files = [URL]()
for case let fileURL as URL in enumerator {
files.append(fileURL)
Expand All @@ -124,8 +126,9 @@ class DataInitializerCoordinator: BPLogger {
let storedIconId = UserDefaults.standard.string(forKey: Constants.UserDefaults.appIcon)
sharedDefaults.set(storedIconId, forKey: Constants.UserDefaults.appIcon)
} else if let sharedAppIcon = sharedDefaults.string(forKey: Constants.UserDefaults.appIcon),
let localAppIcon = UserDefaults.standard.string(forKey: Constants.UserDefaults.appIcon),
sharedAppIcon != localAppIcon {
let localAppIcon = UserDefaults.standard.string(forKey: Constants.UserDefaults.appIcon),
sharedAppIcon != localAppIcon
{
sharedDefaults.set(localAppIcon, forKey: Constants.UserDefaults.appIcon)
UserDefaults.standard.removeObject(forKey: Constants.UserDefaults.appIcon)
}
Expand Down
Loading