Skip to content

Commit

Permalink
Merge pull request #268 from TortugaPower/feature/siri-shortcut
Browse files Browse the repository at this point in the history
Siri shortcut for last played book
  • Loading branch information
GianniCarlo authored Nov 3, 2018
2 parents 471efa9 + a7adc26 commit 32cf4f5
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 55 deletions.
13 changes: 10 additions & 3 deletions BookPlayer.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,18 @@
418B6D051D2707F800F974FB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 418B6D041D2707F800F974FB /* Assets.xcassets */; };
418B6D081D2707F800F974FB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 418B6D061D2707F800F974FB /* LaunchScreen.storyboard */; };
418B6D5D1D2CB76900F974FB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 418B6D5C1D2CB76900F974FB /* PlayerViewController.swift */; };
4197240021874D5F00AB1190 /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419723FF21874D5F00AB1190 /* UserActivityManager.swift */; };
41A6A87520DEECC400B2C621 /* PlaylistTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A6A87420DEECC400B2C621 /* PlaylistTests.swift */; };
41B2AC8E1D43CCE8005382A9 /* ChaptersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41B2AC8D1D43CCE8005382A9 /* ChaptersViewController.swift */; };
41C3CF7E20AEA5E5007C3EF4 /* DataManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41C3CF7D20AEA5E5007C3EF4 /* DataManagerTests.swift */; };
41D39D41215F177D00B65290 /* BookActivityItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41D39D40215F177D00B65290 /* BookActivityItemProvider.swift */; };
41D4F2EB2101A278009F1B1E /* DirectoryWatcher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 41D4F2EA2101A278009F1B1E /* DirectoryWatcher.framework */; };
41D4F2EF21053944009F1B1E /* IndexPath+BookPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41D4F2EE21053944009F1B1E /* IndexPath+BookPlayer.swift */; };
41E34E502138EA8200A3997C /* ImportOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41E34E4F2138EA8200A3997C /* ImportOperation.swift */; };
5CBB29552163E57300E3A9FF /* ZIPFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CBB29522163A17F00E3A9FF /* ZIPFoundation.framework */; };
6906A55021720FDF00A9E0B2 /* BookSortServiceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6906A54F21720FDF00A9E0B2 /* BookSortServiceTest.swift */; };
6906A553217211C600A9E0B2 /* StubFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6906A552217211C600A9E0B2 /* StubFactory.swift */; };
69225C4F2159C2650004739E /* BookSortService+SortError+PlayListSortOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69225C4E2159C2650004739E /* BookSortService+SortError+PlayListSortOrder.swift */; };
5CBB29552163E57300E3A9FF /* ZIPFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CBB29522163A17F00E3A9FF /* ZIPFoundation.framework */; };
69343D332133844D000C425E /* VoiceOverService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69343D322133844D000C425E /* VoiceOverService.swift */; };
69343D36213A07B4000C425E /* VoiceOverServiceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69343D35213A07B4000C425E /* VoiceOverServiceTest.swift */; };
C30B085F209654E3003F325B /* UIColor+BookPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C30B085E209654E3003F325B /* UIColor+BookPlayer.swift */; };
Expand Down Expand Up @@ -160,6 +161,7 @@
418B6D141D2707F800F974FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
418B6D5C1D2CB76900F974FB /* PlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; };
419196341D47CC4E007A3AF3 /* BookPlayer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BookPlayer.entitlements; sourceTree = "<group>"; };
419723FF21874D5F00AB1190 /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = "<group>"; };
41A6A87420DEECC400B2C621 /* PlaylistTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaylistTests.swift; sourceTree = "<group>"; };
41AB1C121D30298800AC1AA0 /* MBProgressHUD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MBProgressHUD.framework; path = Carthage/Build/iOS/MBProgressHUD.framework; sourceTree = "<group>"; };
41B2AC8D1D43CCE8005382A9 /* ChaptersViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChaptersViewController.swift; sourceTree = "<group>"; };
Expand All @@ -168,10 +170,10 @@
41D4F2EA2101A278009F1B1E /* DirectoryWatcher.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DirectoryWatcher.framework; path = Carthage/Build/iOS/DirectoryWatcher.framework; sourceTree = "<group>"; };
41D4F2EE21053944009F1B1E /* IndexPath+BookPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IndexPath+BookPlayer.swift"; sourceTree = "<group>"; };
41E34E4F2138EA8200A3997C /* ImportOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportOperation.swift; sourceTree = "<group>"; };
5CBB29522163A17F00E3A9FF /* ZIPFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ZIPFoundation.framework; path = Carthage/Build/iOS/ZIPFoundation.framework; sourceTree = "<group>"; };
6906A54F21720FDF00A9E0B2 /* BookSortServiceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookSortServiceTest.swift; sourceTree = "<group>"; };
6906A552217211C600A9E0B2 /* StubFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubFactory.swift; sourceTree = "<group>"; };
69225C4E2159C2650004739E /* BookSortService+SortError+PlayListSortOrder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookSortService+SortError+PlayListSortOrder.swift"; sourceTree = "<group>"; };
5CBB29522163A17F00E3A9FF /* ZIPFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ZIPFoundation.framework; path = Carthage/Build/iOS/ZIPFoundation.framework; sourceTree = "<group>"; };
69343D322133844D000C425E /* VoiceOverService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceOverService.swift; sourceTree = "<group>"; };
69343D35213A07B4000C425E /* VoiceOverServiceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceOverServiceTest.swift; sourceTree = "<group>"; };
C30B085E209654E3003F325B /* UIColor+BookPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+BookPlayer.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -349,6 +351,7 @@
children = (
69343D322133844D000C425E /* VoiceOverService.swift */,
69225C4E2159C2650004739E /* BookSortService+SortError+PlayListSortOrder.swift */,
419723FF21874D5F00AB1190 /* UserActivityManager.swift */,
);
path = Services;
sourceTree = "<group>";
Expand Down Expand Up @@ -582,7 +585,7 @@
};
418B6CF71D2707F800F974FB = {
CreatedOnToolsVersion = 7.3;
DevelopmentTeam = FV9NULGJG4;
DevelopmentTeam = S7TJSJXWUZ;
LastSwiftMigration = 0900;
SystemCapabilities = {
com.apple.ApplicationGroups.iOS = {
Expand All @@ -591,6 +594,9 @@
com.apple.BackgroundModes = {
enabled = 1;
};
com.apple.Siri = {
enabled = 1;
};
com.apple.iCloud = {
enabled = 1;
};
Expand Down Expand Up @@ -741,6 +747,7 @@
C30EEEBF2079645C00A4ED33 /* PlayerContainerViewController.swift in Sources */,
C318D3AD208CF624000666F8 /* PlayerJumpIcon.swift in Sources */,
C37A6875209F13120063AEAC /* CreditsViewController.swift in Sources */,
4197240021874D5F00AB1190 /* UserActivityManager.swift in Sources */,
41D4F2EF21053944009F1B1E /* IndexPath+BookPlayer.swift in Sources */,
418B6D5D1D2CB76900F974FB /* PlayerViewController.swift in Sources */,
69343D332133844D000C425E /* VoiceOverService.swift in Sources */,
Expand Down
11 changes: 11 additions & 0 deletions BookPlayer/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// register document's folder listener
self.setupDocumentListener()

if let activityDictionary = launchOptions?[.userActivityDictionary] as? [UIApplicationLaunchOptionsKey: Any],
let activityType = activityDictionary[.userActivityType] as? String,
activityType == Constants.UserActivityPlayback {
defaults.set(true, forKey: activityType)
}

return true
}

Expand All @@ -64,6 +70,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return true
}

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
PlayerManager.shared.play()
return true
}

func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
Expand Down
4 changes: 3 additions & 1 deletion BookPlayer/BookPlayer.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<array>
<string>CloudDocuments</string>
</array>
<key>com.apple.developer.siri</key>
<true/>
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array>
<string>iCloud.$(CFBundleIdentifier)</string>
Expand All @@ -18,7 +20,7 @@
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.tortugapower.bookplayer.files</string>
<string>group.$(CFBundleIdentifier).files</string>
</array>
</dict>
</plist>
3 changes: 3 additions & 0 deletions BookPlayer/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,7 @@ enum Constants {
case normal = 1.0
case boosted = 2.0
}

static let UserActivityPlayback = Bundle.main.bundleIdentifier! + ".activity.playback"
static let ApplicationGroupIdentifier = "group." + Bundle.main.bundleIdentifier! + ".files"
}
4 changes: 4 additions & 0 deletions BookPlayer/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSUserActivityTypes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.playback</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
Expand Down
2 changes: 1 addition & 1 deletion BookPlayer/Library/FileManagement/DataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class DataManager {
}

private static var storeUrl: URL {
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.tortugapower.bookplayer.files")!.appendingPathComponent("BookPlayer.sqlite")
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.ApplicationGroupIdentifier)!.appendingPathComponent("BookPlayer.sqlite")
}

// MARK: - Operations
Expand Down
46 changes: 24 additions & 22 deletions BookPlayer/Library/FileManagement/ImportOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,35 +75,37 @@ class ImportOperation: Operation {

inputStream.open()

let digest = Digest(algorithm: .md5)
autoreleasepool {
let digest = Digest(algorithm: .md5)

while inputStream.hasBytesAvailable {
var inputBuffer = [UInt8](repeating: 0, count: 1024)
inputStream.read(&inputBuffer, maxLength: inputBuffer.count)
_ = digest.update(byteArray: inputBuffer)
}
while inputStream.hasBytesAvailable {
var inputBuffer = [UInt8](repeating: 0, count: 1024)
inputStream.read(&inputBuffer, maxLength: inputBuffer.count)
_ = digest.update(byteArray: inputBuffer)
}

inputStream.close()
inputStream.close()

let finalDigest = digest.final()
let finalDigest = digest.final()

let hash = hexString(fromArray: finalDigest)
let ext = file.originalUrl.pathExtension
let filename = hash + ".\(ext)"
let destinationURL = file.destinationFolder.appendingPathComponent(filename)
let hash = hexString(fromArray: finalDigest)
let ext = file.originalUrl.pathExtension
let filename = hash + ".\(ext)"
let destinationURL = file.destinationFolder.appendingPathComponent(filename)

do {
if !FileManager.default.fileExists(atPath: destinationURL.path) {
try FileManager.default.moveItem(at: file.originalUrl, to: destinationURL)
try (destinationURL as NSURL).setResourceValue(URLFileProtection.none, forKey: .fileProtectionKey)
} else {
try FileManager.default.removeItem(at: file.originalUrl)
do {
if !FileManager.default.fileExists(atPath: destinationURL.path) {
try FileManager.default.moveItem(at: file.originalUrl, to: destinationURL)
try (destinationURL as NSURL).setResourceValue(URLFileProtection.none, forKey: .fileProtectionKey)
} else {
try FileManager.default.removeItem(at: file.originalUrl)
}
} catch {
fatalError("Fail to move file from \(file.originalUrl) to \(destinationURL)")
}
} catch {
fatalError("Fail to move file from \(file.originalUrl) to \(destinationURL)")
}

file.processedUrl = destinationURL
file.processedUrl = destinationURL
}
}
}
}
52 changes: 28 additions & 24 deletions BookPlayer/Library/LibraryViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,36 @@ class LibraryViewController: BaseListViewController, UIGestureRecognizerDelegate
// register for appDelegate openUrl notifications
NotificationCenter.default.addObserver(self, selector: #selector(self.reloadData), name: .reloadData, object: nil)

self.loadLibrary()

// handle CoreData migration into shared app groups
if !UserDefaults.standard.bool(forKey: Constants.UserDefaults.appGroupsMigration.rawValue) {
self.migrateCoreDataStack()
UserDefaults.standard.set(true, forKey: Constants.UserDefaults.appGroupsMigration.rawValue)
}

self.loadLibrary()

self.loadLastBook()
}

// No longer need to deregister observers for iOS 9+!
// https://developer.apple.com/library/mac/releasenotes/Foundation/RN-Foundation/index.html#10_11NotificationCenter
deinit {
// for iOS 8
NotificationCenter.default.removeObserver(self)
UIApplication.shared.endReceivingRemoteControlEvents()
}

func loadLibrary() {
self.library = DataManager.getLibrary()

self.toggleEmptyStateView()

self.tableView.reloadData()

DataManager.notifyPendingFiles()
}

func loadLastBook() {
guard let identifier = UserDefaults.standard.string(forKey: Constants.UserDefaults.lastPlayedBook.rawValue),
let item = self.library.getItem(with: identifier) else {
return
Expand All @@ -56,31 +78,13 @@ class LibraryViewController: BaseListViewController, UIGestureRecognizerDelegate
guard loaded else { return }

NotificationCenter.default.post(name: .playerDismissed, object: nil, userInfo: nil)
if UserDefaults.standard.bool(forKey: Constants.UserActivityPlayback) {
UserDefaults.standard.removeObject(forKey: Constants.UserActivityPlayback)
PlayerManager.shared.play()
}
}
}

// No longer need to deregister observers for iOS 9+!
// https://developer.apple.com/library/mac/releasenotes/Foundation/RN-Foundation/index.html#10_11NotificationCenter
deinit {
// for iOS 8
NotificationCenter.default.removeObserver(self)
UIApplication.shared.endReceivingRemoteControlEvents()
}

/**
* Load local files and process them (rename them if necessary)
* Spaces in file names can cause side effects when trying to load the data
*/
func loadLibrary() {
self.library = DataManager.getLibrary()

self.toggleEmptyStateView()

self.tableView.reloadData()

DataManager.notifyPendingFiles()
}

/**
* Migrates existing stack into the new container app groups.
* In case it fails, it loads all the files from the Processed folder
Expand Down
12 changes: 8 additions & 4 deletions BookPlayer/Player/PlayerManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,9 @@ class PlayerManager: NSObject {
// Set speed for player
audioplayer.rate = self.speed

NotificationCenter.default.post(name: .bookReady, object: nil, userInfo: ["book": book])
UserActivityManager.shared.resumePlaybackActivity()

if #available(iOS 11.0, *) {
MPNowPlayingInfoCenter.default().playbackState = self.isPlaying ? MPNowPlayingPlaybackState.playing : MPNowPlayingPlaybackState.paused
}
NotificationCenter.default.post(name: .bookReady, object: nil, userInfo: ["book": book])

completion(true)
}
Expand Down Expand Up @@ -278,6 +276,8 @@ class PlayerManager: NSObject {
return
}

UserActivityManager.shared.resumePlaybackActivity()

UserDefaults.standard.set(currentBook.identifier, forKey: Constants.UserDefaults.lastPlayedBook.rawValue)

do {
Expand Down Expand Up @@ -342,6 +342,8 @@ class PlayerManager: NSObject {
return
}

UserActivityManager.shared.stopPlaybackActivity()

UserDefaults.standard.set(currentBook.identifier, forKey: Constants.UserDefaults.lastPlayedBook.rawValue)

// Invalidate timer if needed
Expand Down Expand Up @@ -388,6 +390,8 @@ class PlayerManager: NSObject {
func stop() {
self.audioPlayer?.stop()

UserActivityManager.shared.stopPlaybackActivity()

var userInfo: [AnyHashable: Any]?

if let book = self.currentBook {
Expand Down
39 changes: 39 additions & 0 deletions BookPlayer/Services/UserActivityManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// UserActivityManager.swift
// BookPlayer
//
// Created by Gianni Carlo on 10/29/18.
// Copyright © 2018 Tortuga Power. All rights reserved.
//

import Foundation
import Intents

class UserActivityManager {
static let shared = UserActivityManager()
private init() {}

var currentActivity: NSUserActivity?

private func createPlaybackActivity() -> NSUserActivity {
let activity = NSUserActivity(activityType: Constants.UserActivityPlayback)
activity.title = "Continue last played book"
if #available(iOS 12.0, *) {
activity.isEligibleForPrediction = true
activity.persistentIdentifier = NSUserActivityPersistentIdentifier(Constants.UserActivityPlayback)
activity.suggestedInvocationPhrase = "Continue my book"
}
activity.isEligibleForSearch = true

return activity
}

func resumePlaybackActivity() {
self.currentActivity = self.currentActivity ?? self.createPlaybackActivity()
self.currentActivity?.becomeCurrent()
}

func stopPlaybackActivity() {
self.currentActivity?.resignCurrent()
}
}

0 comments on commit 32cf4f5

Please sign in to comment.